Skip to content

Topology (entangling-layer patterns)

Every variational circuit (VQE, QAOA, hardware-efficient ansätze) needs an entangling layer -- a set of two-qubit gates connecting qubits in some pattern -- and hand-writing it as a for loop is one of the most repeated patterns in quantum-circuit code. entangling_layer gives five of the standard patterns a name and a single call instead, the same role Qiskit's TwoLocal(entanglement=...) or PennyLane's qml.broadcast(pattern=...) play.

Step 1. The five patterns, on 4 qubits

from dense_evolution.circuits.topology import entangling_layer

for pattern in ('linear', 'circular', 'full', 'star', 'brick'):
    print(pattern, entangling_layer(4, pattern=pattern))
linear [('cx', 0, 1), ('cx', 1, 2), ('cx', 2, 3)]
circular [('cx', 0, 1), ('cx', 1, 2), ('cx', 2, 3), ('cx', 3, 0)]
full [('cx', 0, 1), ('cx', 0, 2), ('cx', 0, 3), ('cx', 1, 2), ('cx', 1, 3), ('cx', 2, 3)]
star [('cx', 0, 1), ('cx', 0, 2), ('cx', 0, 3)]
brick [('cx', 0, 1), ('cx', 2, 3), ('cx', 1, 2)]

Each call returns a plain gate-tuple list, ready for run_circuit like any hand-built circuit. linear is a chain, circular adds one wraparound edge closing it into a ring, full connects every pair (the most expressive, and the most gates), star routes everything through one hub qubit, and brick alternates even/odd pairs into the staircase pattern behind most Trotterized and hardware-efficient ansätze.

The diagram above is the real brick pattern from Step 1, drawn with de.plot_circuit(entangling_layer(4, pattern='brick'), 4) -- (0,1) and (2,3) fire in the same layer, then (1,2) bridges them in the next.

Step 2. Use one as an ansatz layer

import numpy as np
import dense_evolution as de
from dense_evolution.circuits.topology import entangling_layer

sim = de.DenseSVSimulator(4)
ops = [('h', i) for i in range(4)] + entangling_layer(4, pattern='brick')
sim.run_circuit(ops)
print(round(float(np.sum(sim.get_probabilities())), 6))
1.0

entangling_layer's output concatenates directly onto any other gate list -- here, a layer of H on every qubit (the usual first layer of a hardware-efficient ansatz) followed by one brick entangling layer. The probabilities still sum to 1, the same sanity check worth running on any new ansatz layer before trusting energies computed from it in a real VQE loop.


Details

gate parameter: any two-qubit gate name works ('cx', 'cz', 'cy', or a custom name registered elsewhere) -- it's not validated against dense_evolution.gates.GATES here, so an unregistered name only fails later, at run_circuit time.

reverse=True swaps (control, target) to (target, control) on every edge in the pattern -- some ansätze alternate direction layer to layer for symmetry.

hub parameter only affects pattern='star': it picks which qubit every other qubit connects to (default 0).

This package's DenseSVSimulator has no notion of hardware connectivity at all -- any two qubits can always interact directly, regardless of index distance. These five patterns are an ansatz-design convenience (fewer parameters, known symmetry), not a constraint the simulator enforces the way a real superconducting chip's physical layout would.

See also: States -- ghz_state builds its cx chain with entangling_layer(pattern='linear') directly, the simplest possible use of this module.

topology

Entangling-layer topology helpers.

Every variational circuit (VQE, QAOA, hardware-efficient ansätze) needs an entangling layer, and hand-writing it as a for loop of two-qubit gates is one of the most repeated patterns across quantum-circuit code. Other libraries give it a name and a single call instead (Qiskit's TwoLocal(entanglement=...), PennyLane's qml.broadcast(pattern=...)). entangling_layer is the Dense-Evolution equivalent: it returns a plain list of gate tuples in the circuit format run_circuit already accepts, so it drops straight into any existing circuit list via concatenation.

entangling_layer

entangling_layer(
    n_qubits,
    pattern="linear",
    gate="cx",
    reverse=False,
    hub=0,
)

Build a list of two-qubit gate tuples connecting n_qubits according to a named topology.

Patterns

'linear' -- chain: (0,1), (1,2), ..., (n-2,n-1) 'circular' -- linear + one wraparound edge (n-1,0) ("ring"); identical to 'linear' when n_qubits == 2, since there is only one possible edge between two qubits 'full' -- every pair (i,j) with i<j ("complete"/all-to-all) 'star' -- a single hub qubit connected to every other qubit 'brick' -- alternating even/odd layers: (0,1)(2,3).. then (1,2)(3,4).. ("brickwork"/staircase, the pattern behind most Trotterized and hardware-efficient ansätze)

Parameters:

Name Type Description Default
n_qubits int

Number of qubits involved, must be >= 2.

required
pattern str

One of VALID_PATTERNS.

'linear'
gate str

Two-qubit gate name applied to every edge (e.g. 'cx', 'cz', 'cy'). Not validated against dense_evolution.gates.GATES here, so a custom gate name registered elsewhere still works.

'cx'
reverse bool

Swap (control, target) -> (target, control) for every edge. Some ansätze alternate direction layer to layer for symmetry.

False
hub int

Hub qubit index, only used by pattern='star'.

0

Returns:

Type Description
list[tuple[str, int, int]]
Source code in dense_evolution/circuits/topology.py
def entangling_layer(n_qubits, pattern='linear', gate='cx', reverse=False, hub=0):
    """
    Build a list of two-qubit gate tuples connecting n_qubits according to
    a named topology.

    Patterns
    --------
    'linear'   -- chain: (0,1), (1,2), ..., (n-2,n-1)
    'circular' -- linear + one wraparound edge (n-1,0) ("ring"); identical
                  to 'linear' when n_qubits == 2, since there is only one
                  possible edge between two qubits
    'full'     -- every pair (i,j) with i<j ("complete"/all-to-all)
    'star'     -- a single hub qubit connected to every other qubit
    'brick'    -- alternating even/odd layers: (0,1)(2,3).. then (1,2)(3,4)..
                  ("brickwork"/staircase, the pattern behind most Trotterized
                  and hardware-efficient ansätze)

    Parameters
    ----------
    n_qubits : int
        Number of qubits involved, must be >= 2.
    pattern : str
        One of VALID_PATTERNS.
    gate : str
        Two-qubit gate name applied to every edge (e.g. 'cx', 'cz', 'cy').
        Not validated against dense_evolution.gates.GATES here, so a custom
        gate name registered elsewhere still works.
    reverse : bool
        Swap (control, target) -> (target, control) for every edge. Some
        ansätze alternate direction layer to layer for symmetry.
    hub : int
        Hub qubit index, only used by pattern='star'.

    Returns
    -------
    list[tuple[str, int, int]]
    """
    if n_qubits < 2:
        raise ValueError(f"entangling_layer needs at least 2 qubits, got {n_qubits}")
    if pattern not in VALID_PATTERNS:
        raise ValueError(f"unknown pattern {pattern!r}, expected one of {VALID_PATTERNS}")

    if pattern == 'linear':
        edges = [(q, q + 1) for q in range(n_qubits - 1)]
    elif pattern == 'circular':
        edges = [(q, q + 1) for q in range(n_qubits - 1)]
        if n_qubits > 2:
            edges.append((n_qubits - 1, 0))
    elif pattern == 'full':
        edges = [(i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)]
    elif pattern == 'star':
        if not (0 <= hub < n_qubits):
            raise ValueError(f"hub={hub} out of range for n_qubits={n_qubits}")
        edges = [(hub, q) for q in range(n_qubits) if q != hub]
    elif pattern == 'brick':
        even = [(q, q + 1) for q in range(0, n_qubits - 1, 2)]
        odd = [(q, q + 1) for q in range(1, n_qubits - 1, 2)]
        edges = even + odd

    if reverse:
        edges = [(b, a) for a, b in edges]

    return [(gate, a, b) for a, b in edges]