Skip to content

Custom Quantum Gates

Create custom quantum gates from unitary matrices, build oracles, and compose multi-controlled operations with pyqpanda3.


Problem

pyqpanda3 ships with 37+ built-in gates covering standard operations -- Pauli gates, rotations, CNOT, SWAP, TOFFOLI, and more. However, several important scenarios demand gates beyond the standard set:

  1. Problem-specific unitaries. An algorithm may require a unitary that has no standard name, such as a custom coin operator for quantum walks or a structured mixing operator for QAOA variants.

  2. Oracle construction. Grover's search, Deutsch-Jozsa, and other oracle-based algorithms need phase-flip or bit-flip oracles derived from a classical function's truth table.

  3. Non-standard gate sets. When compiling to specific hardware with a limited native gate set, you may need to define custom decomposition building blocks.

  4. Subroutine re-use. You may want to package a frequently used gate sequence into a single object that can be inverted (.dagger()), controlled (.control()), or embedded in multiple programs.

In all of these cases, you need the ability to define a gate from its unitary matrix or compose one from existing primitives.


Solution

pyqpanda3 provides two primary mechanisms for creating custom gates, plus composition tools for building complex operations from simpler parts:

Step 1: Define the unitary matrix

Every quantum gate is described by a unitary matrix U satisfying UU=I. For an n-qubit gate, the matrix has dimension 2n×2n. Construct it using numpy as a complex-valued array.

Step 2: Create the gate

Use core.Oracle(qubits, matrix) to instantiate a gate from an arbitrary unitary matrix. The function validates that the matrix is unitary and that its dimensions match the number of target qubits.

For named gates with standard parameters, use core.create_gate(name, qubits, params).

Step 3: Compose and control

Apply .control(qubit) to add a control qubit, .dagger() to obtain the inverse, and embed the result in a QCircuit or QProg using the << operator.

Step 4: Verify and execute

Use .matrix() to retrieve the gate's matrix representation and verify correctness. Then execute on CPUQVM or another simulator backend.


Code

Example 1: Single-qubit gate from a 2x2 unitary matrix

Create a custom single-qubit rotation around an axis in the X-Z plane. The gate applies the rotation

Rn^(θ)=eiθn^σ/2

where n^=(sinα,0,cosα) is a unit vector in the X-Z plane.

python
import numpy as np
from pyqpanda3 import core

# Define a custom rotation axis and angle
alpha = np.pi / 6   # 30 degrees from Z toward X
theta = np.pi / 3   # rotation angle

# Build the 2x2 unitary matrix
# R_{hat_n}(theta) = cos(theta/2)*I - i*sin(theta/2)*(sin(alpha)*X + cos(alpha)*Z)
cos_half = np.cos(theta / 2)
sin_half = np.sin(theta / 2)

custom_matrix = np.array([
    [cos_half - 1j * sin_half * np.cos(alpha),
     -1j * sin_half * np.sin(alpha)],
    [-1j * sin_half * np.sin(alpha),
     cos_half + 1j * sin_half * np.cos(alpha)]
], dtype=complex)

# Create the custom gate on qubit 0
qubits = [0]
custom_gate = core.Oracle(qubits, custom_matrix)

# Verify unitarity
mat = custom_gate.matrix()
product = mat @ mat.conj().T
is_unitary = np.allclose(product, np.eye(2))
print(f"Gate is unitary: {is_unitary}")  # True

# Use in a circuit
prog = core.QProg()
prog << custom_gate
prog << core.measure([0], [0])

# Execute
machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")

Example 2: Two-qubit gate from a 4x4 unitary matrix

Implement the square-root-of-SWAP gate, which is a standard gate in quantum computing but not always available as a primitive:

SWAP=(100001+i21i2001i21+i200001)
python
import numpy as np
from pyqpanda3 import core

# Define the sqrt(SWAP) matrix
a = (1 + 1j) / 2   # (1+i)/2
b = (1 - 1j) / 2   # (1-i)/2

sqrt_swap_matrix = np.array([
    [1, 0, 0, 0],
    [0, a, b, 0],
    [0, b, a, 0],
    [0, 0, 0, 1]
], dtype=complex)

# Create the gate acting on qubits 1 and 2
sqrt_swap_gate = core.Oracle([1, 2], sqrt_swap_matrix)

# Verify: applying it twice should give a full SWAP
mat = sqrt_swap_gate.matrix()
double_mat = mat @ mat
swap_matrix = np.array([
    [1, 0, 0, 0],
    [0, 0, 1, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 1]
], dtype=complex)
print(f"sqrt(SWAP)^2 == SWAP: {np.allclose(double_mat, swap_matrix)}")  # True

# Use in a program
prog = core.QProg()
prog << core.X(1)              # prepare |10>
prog << sqrt_swap_gate         # apply sqrt(SWAP)
prog << core.measure([1, 2], [0, 1])

machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")

Example 3: Multi-controlled gate using .control()

Build a multi-controlled Z gate (CCZ) by chaining .control() on a Z gate. The CCZ gate applies a phase flip only when all control qubits are |1:

CCZ|x0,x1,x2=(1)x0x1x2|x0,x1,x2
python
import numpy as np
from pyqpanda3 import core

# Method 1: Chain .control() calls
z_gate = core.Z(2)          # Z on target qubit 2
cz_gate = z_gate.control(1)  # add control qubit 1
ccz_gate = cz_gate.control(0)  # add second control qubit 0

# Verify the 8x8 matrix
mat = ccz_gate.matrix()
# CCZ should have -1 at position [7,7] (all qubits |111>)
# and +1 on all other diagonal entries
expected = np.eye(8, dtype=complex)
expected[7, 7] = -1
print(f"CCZ matrix correct: {np.allclose(mat, expected)}")  # True

# Method 2: Use the vector form of control()
ccz_gate2 = core.Z(2).control([0, 1])

# Use in a program: prepare |111> and apply CCZ
prog = core.QProg()
prog << core.X(0) << core.X(1) << core.X(2)  # |111>
prog << ccz_gate
prog << core.measure([0, 1, 2], [0, 1, 2])

machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")
# Always measures 111 (CCZ only changes phase, not computational basis)

Example 4: Custom oracle for Grover's algorithm

Build a phase oracle that marks a specific computational basis state by flipping its phase. This is the core subroutine in Grover's search.

python
import numpy as np
from pyqpanda3 import core

def phase_oracle(n_qubits: int, target_state: int) -> core.QGate:
    """Create a phase oracle that marks |target_state> with a -1 phase.

    Args:
        n_qubits: Number of qubits in the search space.
        target_state: Integer index of the state to mark (0 to 2^n - 1).

    Returns:
        A QGate implementing the phase oracle.
    """
    dim = 2 ** n_qubits
    matrix = np.eye(dim, dtype=complex)
    matrix[target_state, target_state] = -1
    qubits = list(range(n_qubits))
    return core.Oracle(qubits, matrix)


def grover_diffusion(n_qubits: int) -> core.QCircuit:
    """Build the Grover diffusion operator (2|s><s| - I).

    This amplifies the amplitude of the marked state.
    """
    dim = 2 ** n_qubits
    # |s> = uniform superposition, so |s><s| = (1/dim) * ones matrix
    matrix = (2.0 / dim) * np.ones((dim, dim), dtype=complex) - np.eye(dim, dtype=complex)
    qubits = list(range(n_qubits))

    circ = core.QCircuit()
    oracle_as_diffusion = core.Oracle(qubits, matrix)
    circ << oracle_as_diffusion
    return circ


# --- Run Grover's search for |101> (state 5) on 3 qubits ---
n = 3
target = 5  # binary 101

# Build the oracle and diffusion operator
oracle = phase_oracle(n, target)
diffusion = grover_diffusion(n)

# Number of Grover iterations (optimal for unstructured search)
iterations = int(np.round(np.pi / 4 * np.sqrt(2 ** n)))
print(f"Grover iterations: {iterations}")

# Construct the full program
prog = core.QProg()

# Step 1: Initialize uniform superposition
for q in range(n):
    prog << core.H(q)

# Step 2: Apply Grover iterations
for _ in range(iterations):
    prog << oracle
    prog << diffusion

# Step 3: Measure
prog << core.measure(list(range(n)), list(range(n)))

# Execute
machine = core.CPUQVM()
machine.run(prog, 1000)
counts = machine.result().get_counts()
print(f"Grover search results: {counts}")
# Should heavily favor the target state 101 (5)

The circuit structure for a single Grover iteration is:

Example 5: Inverse gates using .dagger()

The .dagger() method returns the Hermitian conjugate (adjoint) of a gate, which is its inverse for unitary operations. This is essential for uncomputation patterns.

python
import numpy as np
from pyqpanda3 import core

# Create a custom gate
theta = 1.234
custom_matrix = np.array([
    [np.cos(theta / 2), -1j * np.sin(theta / 2)],
    [-1j * np.sin(theta / 2), np.cos(theta / 2)]
], dtype=complex)

gate = core.Oracle([0], custom_matrix)
gate_inv = gate.dagger()

# Verify that gate * dagger == identity
mat = gate.matrix()
mat_inv = gate_inv.matrix()
product = mat @ mat_inv
print(f"gate * dagger == I: {np.allclose(product, np.eye(2))}")  # True

# --- Uncomputation pattern ---
# Use an auxiliary qubit to compute XOR, then uncompute
prog = core.QProg()
prog << core.H(0)                       # superposition on qubit 0
prog << core.X(1)                       # qubit 1 = |1>

# Compute: XOR of qubits 0 and 1 onto auxiliary qubit 2
compute = core.QCircuit()
compute << core.CNOT(0, 2) << core.CNOT(1, 2)
prog << compute

# Use the auxiliary qubit (apply a phase based on it)
prog << core.Z(2)

# Uncompute: restore auxiliary qubit to |0>
prog << compute.dagger()

prog << core.measure([0, 1, 2], [0, 1, 2])

machine = core.CPUQVM()
machine.run(prog, 1000)
counts = machine.result().get_counts()
print(f"Results: {counts}")
# Qubit 2 (auxiliary) should always measure 0

Example 6: Verifying gate unitarity

Before using a custom matrix in an oracle, verify that it satisfies the unitarity condition UU=I.

python
import numpy as np
from pyqpanda3 import core


def is_unitary(matrix: np.ndarray, atol: float = 1e-10) -> bool:
    """Check whether a matrix is unitary within tolerance.

    A matrix U is unitary if U^dagger * U = I.
    """
    n = matrix.shape[0]
    if matrix.shape != (n, n):
        return False
    product = matrix.conj().T @ matrix
    return np.allclose(product, np.eye(n), atol=atol)


def random_unitary(n: int) -> np.ndarray:
    """Generate a random n x n unitary matrix using QR decomposition."""
    # Generate a random complex matrix
    z = (np.random.randn(n, n) + 1j * np.random.randn(n, n)) / np.sqrt(2)
    # QR decomposition: Q is unitary
    q, r = np.linalg.qr(z)
    # Ensure consistent phase (diagonal of R is real-positive)
    d = np.diag(r)
    ph = d / np.abs(d)
    q = q @ np.diag(ph)
    return q


# Generate and verify a random single-qubit unitary
U1 = random_unitary(2)
print(f"Random 2x2 is unitary: {is_unitary(U1)}")  # True

# Create a gate from it
gate = core.Oracle([0], U1)
mat = gate.matrix()
print(f"Gate matrix matches input: {np.allclose(mat, U1)}")  # True

# Generate and verify a random two-qubit unitary
U2 = random_unitary(4)
print(f"Random 4x4 is unitary: {is_unitary(U2)}")  # True

gate2 = core.Oracle([0, 1], U2)
mat2 = gate2.matrix()
print(f"Gate matrix matches input: {np.allclose(mat2, U2)}")  # True

# The Oracle constructor itself validates unitarity and raises on failure
try:
    bad_matrix = np.array([[1, 0], [1, 0]], dtype=complex)
    core.Oracle([0], bad_matrix)
except (ValueError, RuntimeError) as e:
    print(f"Oracle rejected non-unitary matrix: {e}")

Example 7: Building a Toffoli gate from basic gates

The Toffoli gate (CCX) is a doubly-controlled X gate. While pyqpanda3 provides core.TOFFOLI() directly, constructing one from simpler gates demonstrates the decomposition pattern that applies to any multi-controlled operation.

Toffoli(a,b,c)=|a|b|c(ab)
python
import numpy as np
from pyqpanda3 import core

def toffoli_decomposed(ctrl1: int, ctrl2: int, target: int) -> core.QCircuit:
    """Build a Toffoli gate from H, T, Tdg, CNOT gates.

    This uses the standard decomposition with 6 CNOT gates.
    """
    circ = core.QCircuit()
    # Step 1: H on target
    circ << core.H(target)
    # Step 2: CNOT(ctrl2, target)
    circ << core.CNOT(ctrl2, target)
    # Step 3: Tdg on target
    circ << core.T(target).dagger()
    # Step 4: CNOT(ctrl1, target)
    circ << core.CNOT(ctrl1, target)
    # Step 5: T on target
    circ << core.T(target)
    # Step 6: CNOT(ctrl2, target)
    circ << core.CNOT(ctrl2, target)
    # Step 7: Tdg on target
    circ << core.T(target).dagger()
    # Step 8: CNOT(ctrl1, target)
    circ << core.CNOT(ctrl1, target)
    # Step 9: T on ctrl2 and target
    circ << core.T(ctrl2)
    circ << core.T(target)
    # Step 10: H on target
    circ << core.H(target)
    # Step 11: CNOT(ctrl1, ctrl2)
    circ << core.CNOT(ctrl1, ctrl2)
    # Step 12: T on ctrl1, Tdg on ctrl2
    circ << core.T(ctrl1)
    circ << core.T(ctrl2).dagger()
    # Step 13: CNOT(ctrl1, ctrl2)
    circ << core.CNOT(ctrl1, ctrl2)

    return circ


# Build the decomposed Toffoli
decomposed = toffoli_decomposed(0, 1, 2)

# Compare with the native Toffoli
native_toffoli = core.TOFFOLI(0, 1, 2)

# Both should produce the same 8x8 matrix (up to global phase)
mat_decomp = decomposed.matrix()
mat_native = native_toffoli.matrix()

# Check equivalence (allow global phase difference)
phase = mat_decomp[0, 0] / mat_native[0, 0] if abs(mat_native[0, 0]) > 1e-10 else 1.0
equivalent = np.allclose(mat_decomp, phase * mat_native)
print(f"Decomposed Toffoli == native Toffoli (up to phase): {equivalent}")  # True

# Use in a program
prog = core.QProg()
prog << core.X(0) << core.X(1)  # controls = |11>
prog << decomposed              # target flips from |0> to |1>
prog << core.measure([0, 1, 2], [0, 1, 2])

machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")
# Should always measure 111

The decomposition structure:

Example 8: Custom rotation gate

Implement a rotation gate around an arbitrary Bloch-sphere axis (nx,ny,nz):

Rn^(θ)=cosθ2Iisinθ2(nxX+nyY+nzZ)
python
import numpy as np
from pyqpanda3 import core


def rotation_gate(axis: tuple, angle: float, qubit: int) -> core.QGate:
    """Create a rotation gate around an arbitrary axis.

    Args:
        axis: A unit vector (nx, ny, nz) specifying the rotation axis.
        angle: Rotation angle in radians.
        qubit: Target qubit index.

    Returns:
        A QGate implementing the rotation.
    """
    nx, ny, nz = axis
    norm = np.sqrt(nx**2 + ny**2 + nz**2)
    assert abs(norm - 1.0) < 1e-10, f"Axis must be a unit vector, got norm={norm}"

    cos_half = np.cos(angle / 2)
    sin_half = np.sin(angle / 2)

    # Pauli matrices
    I = np.eye(2, dtype=complex)
    X = np.array([[0, 1], [1, 0]], dtype=complex)
    Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
    Z = np.array([[1, 0], [0, -1]], dtype=complex)

    # R_n(theta) = cos(theta/2)*I - i*sin(theta/2)*(nx*X + ny*Y + nz*Z)
    matrix = cos_half * I - 1j * sin_half * (nx * X + ny * Y + nz * Z)

    return core.Oracle([qubit], matrix)


# Rotate around axis (1/sqrt(3), 1/sqrt(3), 1/sqrt(3)) by pi/2
axis = (1/np.sqrt(3), 1/np.sqrt(3), 1/np.sqrt(3))
gate = rotation_gate(axis, np.pi / 2, 0)

# Verify: rotation by 2*pi should give -I (up to global phase)
full_rotation = rotation_gate(axis, 2 * np.pi, 0)
mat = full_rotation.matrix()
print(f"R(2pi) ≈ -I: {np.allclose(mat, -np.eye(2))}")  # True

# Use the custom rotation in a circuit
prog = core.QProg()
prog << core.H(0)                          # |+>
prog << rotation_gate(axis, np.pi / 4, 0)  # custom rotation
prog << core.measure([0], [0])

machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")

# Chain with .control() to create a controlled version
controlled_rot = gate.control(1)
prog2 = core.QProg()
prog2 << core.H(1)          # control in superposition
prog2 << core.H(0)          # target in superposition
prog2 << controlled_rot     # controlled rotation
prog2 << core.measure([0, 1], [0, 1])

machine.run(prog2, 1000)
print(f"Controlled rotation results: {machine.result().get_counts()}")

Example 9: create_gate() for named gate construction

The core.create_gate() function constructs a gate by name, qubits, and parameters. This is useful for dynamic gate creation where the gate type is determined at runtime.

python
from pyqpanda3 import core

# Create gates by name (string matching the gate type)
h_gate = core.create_gate("H", [0], [])
rx_gate = core.create_gate("RX", [1], [1.5708])  # RX with pi/2
cnot_gate = core.create_gate("CNOT", [0, 1], [])

# Verify they match the factory-function versions
print(f"H matrix match: {len(h_gate.matrix()) == len(core.H(0).matrix())}")
print(f"Gate name: {h_gate.name()}")      # "H"
print(f"Gate name: {rx_gate.name()}")     # "RX"
print(f"Parameters: {rx_gate.parameters()}")  # [1.5708]

# Build a circuit using dynamically created gates
prog = core.QProg()
prog << h_gate
prog << cnot_gate
prog << rx_gate
prog << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")

Example 10: Complete workflow -- custom Grover oracle with verification

This example puts everything together: define a custom oracle, verify it, run Grover's algorithm, and check the results.

python
import numpy as np
from pyqpanda3 import core


def make_bitflip_oracle(n_qubits: int, marked_items: list[int]) -> core.QGate:
    """Create a bit-flip oracle that flips the target qubit for marked items.

    Unlike the phase oracle, this oracle applies X to an ancilla qubit.
    The matrix has the form diag(I_ancilla ⊗ U_f) where U_f flips for
    marked states.

    For simplicity, this returns the full (n+1)-qubit matrix as an Oracle.
    """
    total = n_qubits + 1  # n data qubits + 1 ancilla
    dim = 2 ** total
    matrix = np.eye(dim, dtype=complex)

    for item in marked_items:
        # For each marked computational-basis state, flip the ancilla
        # State |item>|0> maps to |item>|1> and vice versa
        idx0 = item * 2       # |item>|0>
        idx1 = item * 2 + 1   # |item>|1>
        matrix[idx0, idx0] = 0
        matrix[idx0, idx1] = 1
        matrix[idx1, idx0] = 1
        matrix[idx1, idx1] = 0

    qubits = list(range(total))
    return core.Oracle(qubits, matrix)


def grover_phase_oracle(n_qubits: int, marked_items: list[int]) -> core.QGate:
    """Create a phase oracle marking multiple items with a -1 phase."""
    dim = 2 ** n_qubits
    matrix = np.eye(dim, dtype=complex)
    for item in marked_items:
        matrix[item, item] = -1
    qubits = list(range(n_qubits))
    return core.Oracle(qubits, matrix)


def grover_diffusion_matrix(n_qubits: int) -> np.ndarray:
    """Build the Grover diffusion operator as a matrix."""
    dim = 2 ** n_qubits
    return np.eye(dim, dtype=complex) * (-1) + (2.0 / dim) * np.ones((dim, dim), dtype=complex)


# --- Full Grover example ---
n = 3
marked = [2, 5]  # mark states |010> and |101>

# Build oracle and diffusion
oracle = grover_phase_oracle(n, marked)
diff_matrix = grover_diffusion_matrix(n)
diffusion = core.Oracle(list(range(n)), diff_matrix)

# Calculate optimal number of iterations
theta = np.arcsin(len(marked) / 2**n)
iterations = max(1, int(np.round(np.pi / (2 * theta) - 0.5)))
print(f"Marked states: {marked}")
print(f"Theta: {theta:.4f} rad")
print(f"Optimal iterations: {iterations}")

# Build program
prog = core.QProg()

# Initialize uniform superposition
for q in range(n):
    prog << core.H(q)

# Grover iterations
for _ in range(iterations):
    prog << oracle
    prog << diffusion

# Measure
prog << core.measure(list(range(n)), list(range(n)))

# Execute
machine = core.CPUQVM()
machine.run(prog, 1000)
counts = machine.result().get_counts()

print(f"\nGrover results (marking states {marked}):")
for state, count in sorted(counts.items(), key=lambda x: -x[1]):
    print(f"  |{state}> : {count}")

Explanation

When to use custom gates vs built-in gates

ScenarioRecommended approach
Standard gates (H, X, RX, CNOT, etc.)Use built-in factory functions: core.H(0), core.RX(0, theta)
Most general single-qubit gateUse core.U4(0, a, b, c, d) with four parameters
Arbitrary unitary on 1-2 qubitsUse core.Oracle(qubits, matrix) with a numpy matrix
Gate determined at runtimeUse core.create_gate(name, qubits, params)
Multi-controlled standard gateUse gate.control(qubit) chaining
Complex subroutineCompose a QCircuit, then use .dagger() and .control()

As a rule, prefer built-in gates whenever they suffice. They are optimized in the simulator backend and produce cleaner OriginIR output. Reserve core.Oracle() for cases where no standard gate matches your required unitary.

Unitary matrix requirements

The core.Oracle() function enforces two constraints:

  1. Unitarity: The matrix U must satisfy UU=I. If the matrix is not unitary, the constructor raises an exception.

  2. Dimension matching: For n target qubits, the matrix must be 2n×2n. For example, a gate on 2 qubits requires a 4×4 matrix.

  3. No duplicate qubits: Each qubit index in the qubits list must be unique.

To generate a valid unitary matrix programmatically, use the QR decomposition of a random complex matrix:

python
def random_unitary(dim):
    z = (np.random.randn(dim, dim) + 1j * np.random.randn(dim, dim)) / np.sqrt(2)
    q, r = np.linalg.qr(z)
    d = np.diag(r)
    ph = d / np.abs(d)
    return q @ np.diag(ph)

Or construct structured unitaries from known decompositions (Euler angles for single-qubit gates, KAK decomposition for two-qubit gates).

Decomposition strategies for large unitaries

For gates acting on 3 or more qubits, the unitary matrix grows as 2n×2n, which becomes impractical to specify directly. Use these strategies instead:

  1. Analytical decomposition. If you know the circuit structure (e.g., QFT), compose it from standard gates using QCircuit rather than specifying a matrix.

  2. Controlled operations. Build multi-controlled gates by chaining .control() on simpler gates. For example, a CCCCZ (4-controlled Z) is just Z(target).control(c1).control(c2).control(c3).control(c4).

  3. Oracle construction patterns. For phase oracles, construct the identity matrix and flip the sign at the target indices. For bit-flip oracles, swap the appropriate rows/columns.

  4. Circuit synthesis. For arbitrary unitaries on 2 qubits, the KAK (Cartan) decomposition requires at most 3 CNOT gates. For larger unitaries, the Cosine-Sine Decomposition (CSD) recursively breaks down the problem.

Oracle construction patterns

Two common oracle patterns arise in quantum algorithms:

Phase oracle -- marks target states with a 1 phase:

Of|x=(1)f(x)|x

where f(x)=1 for marked items and 0 otherwise. Implementation:

python
def phase_oracle(n, marked):
    dim = 2 ** n
    matrix = np.eye(dim, dtype=complex)
    for m in marked:
        matrix[m, m] = -1
    return core.Oracle(list(range(n)), matrix)

Bit-flip oracle -- flips an ancilla qubit based on the function value:

Of|x|b=|x|bf(x)

This is constructed by swapping rows in the matrix to XOR the ancilla with the function output.

Performance considerations

  1. Matrix size limits. core.Oracle() stores the full 2n×2n matrix. For n>10, the matrix occupies significant memory (220 complex numbers = 16 MB for n=10). Prefer gate-level composition for large qubit counts.

  2. Simulator overhead. The CPUQVM simulator applies Oracle gates via direct matrix multiplication on the state vector. Larger Oracle matrices lead to slower state evolution. Decomposing a large Oracle into a circuit of smaller gates (CNOT + single-qubit) can be faster due to sparse gate application.

  3. .dagger() cost. Calling .dagger() on a QGate returns a new gate with a flag set. The matrix is computed lazily when .matrix() is called. On a QCircuit, .dagger() reverses the gate order and daggers each gate individually, which is efficient.

  4. .control() cost. Adding a control qubit expands the gate matrix from 2n×2n to 2n+1×2n+1. The matrix has the block structure:

Controlled-U=(I00U)

For deeply chained controls, consider whether a TOFFOLI-based decomposition would be more efficient.

  1. Matrix retrieval. The .matrix() method computes the matrix representation on demand. For gates with known decompositions (e.g., H, RX), this is fast. For Oracle gates, it returns the stored matrix directly. For circuits, it multiplies all constituent gate matrices, which is expensive for large circuits.

API reference summary

APIPurposeSignature
core.Oracle()Gate from unitary matrixOracle(qubits: list, matrix: np.ndarray) -> QGate
core.create_gate()Gate by name and paramscreate_gate(name: str, qubits: list, params: list) -> QGate
gate.dagger()Inverse gateReturns a new QGate
gate.control(qubit)Add one control qubitReturns new QGate
gate.control([q1, q2])Add multiple controlsReturns new QGate
gate.matrix()Get unitary matrixmatrix(expanded=False) -> np.ndarray
circuit.dagger()Inverse circuitReturns a new QCircuit
circuit.control([qubits])Controlled circuitReturns new QCircuit
circuit.matrix()Circuit unitaryReturns np.ndarray

Next steps:

Released under the MIT License.