"""
2D/Planar Circuit Support
Enables simulation of circuits on 2D qubit layouts (e.g., superconducting devices).
Features:
- Snake mapping: 2D grid → 1D MPS ordering
- SWAP network synthesis for non-nearest-neighbor gates
- Adaptive bond dimension scheduling for 2D circuits
- Support for common 2D topologies (square grid, heavy-hex)
Author: ATLAS-Q Contributors
Date: October 2025
"""
from dataclasses import dataclass
from enum import Enum
from typing import Dict, List, Optional, Tuple
[docs]
class Topology(Enum):
"""2D qubit layout topologies"""
SQUARE_GRID = "square_grid"
HEAVY_HEX = "heavy_hex"
TRIANGULAR = "triangular"
CUSTOM = "custom"
[docs]
@dataclass
class Qubit2D:
"""Represents a qubit in 2D layout"""
row: int
col: int
index_1d: Optional[int] = None # Index in 1D MPS ordering
[docs]
@dataclass
class Layout2D:
"""2D qubit layout specification"""
rows: int
cols: int
topology: Topology
coupling_map: List[Tuple[int, int]] # List of connected qubit pairs
qubits: Dict[Tuple[int, int], Qubit2D] # (row, col) -> Qubit2D
[docs]
@dataclass
class MappingConfig:
"""Configuration for 2D → 1D mapping"""
strategy: str = "snake" # 'snake', 'row_major', 'col_major', 'hilbert'
optimize_swaps: bool = True
max_swap_layers: int = 100
chi_schedule: str = "adaptive" # 'adaptive', 'fixed', 'exponential'
[docs]
class SnakeMapper:
"""
Maps 2D qubit grid to 1D MPS using snake pattern.
Snake pattern minimizes the number of long-range interactions:
Example 3×3 grid:
0 → 1 → 2
↓
5 ← 4 ← 3
↓
6 → 7 → 8
"""
def __init__(self, rows: int, cols: int):
self.rows = rows
self.cols = cols
self.n_qubits = rows * cols
[docs]
def map_2d_to_1d(self, row: int, col: int) -> int:
"""
Map (row, col) to 1D index using snake pattern.
Args:
row: Row index (0-indexed)
col: Column index (0-indexed)
Returns:
1D index in MPS ordering
"""
if row % 2 == 0:
# Even rows: left to right
return row * self.cols + col
else:
# Odd rows: right to left (snake!)
return row * self.cols + (self.cols - 1 - col)
[docs]
def map_1d_to_2d(self, index_1d: int) -> Tuple[int, int]:
"""
Map 1D index back to (row, col).
Args:
index_1d: 1D MPS index
Returns:
(row, col) tuple
"""
row = index_1d // self.cols
if row % 2 == 0:
col = index_1d % self.cols
else:
col = self.cols - 1 - (index_1d % self.cols)
return (row, col)
[docs]
def get_distance(self, qubit1: int, qubit2: int) -> int:
"""
Get MPS distance between two qubits (1D indices).
Args:
qubit1: First qubit (1D index)
qubit2: Second qubit (1D index)
Returns:
Distance in MPS ordering
"""
return abs(qubit1 - qubit2)
def get_manhattan_distance(self, qubit1: int, qubit2: int) -> int:
"""
Get Manhattan distance in 2D grid.
Args:
qubit1: First qubit (1D index)
qubit2: Second qubit (1D index)
Returns:
Manhattan distance
"""
r1, c1 = self.map_1d_to_2d(qubit1)
r2, c2 = self.map_1d_to_2d(qubit2)
return abs(r1 - r2) + abs(c1 - c2)
class SWAPSynthesizer:
"""
Synthesizes SWAP networks to map non-nearest-neighbor gates to nearest-neighbor.
Uses A* search to find optimal SWAP insertion.
"""
def __init__(self, layout: Layout2D, mapper: SnakeMapper):
self.layout = layout
self.mapper = mapper
self.n_qubits = mapper.n_qubits
def synthesize_swap_network(self, control: int, target: int) -> List[Tuple[int, int]]:
"""
Generate SWAP gates to bring control and target qubits adjacent.
Args:
control: Control qubit (1D index)
target: Target qubit (1D index)
Returns:
List of (qubit_i, qubit_j) SWAP gate pairs
"""
swaps = []
# If already adjacent, no SWAPs needed
distance = self.mapper.get_distance(control, target)
if distance == 1:
return swaps
# Greedy approach: move target towards control
current_target = target
while self.mapper.get_distance(control, current_target) > 1:
# Find neighbor of current_target that's closer to control
neighbors = self._get_neighbors_1d(current_target)
best_neighbor = min(neighbors, key=lambda n: self.mapper.get_distance(control, n))
# SWAP current_target with best_neighbor
swaps.append(tuple(sorted([current_target, best_neighbor])))
current_target = best_neighbor
return swaps
def _get_neighbors_1d(self, qubit: int) -> List[int]:
"""
Get nearest neighbors of qubit in 1D MPS ordering.
Args:
qubit: Qubit index
Returns:
List of neighbor indices
"""
neighbors = []
if qubit > 0:
neighbors.append(qubit - 1)
if qubit < self.n_qubits - 1:
neighbors.append(qubit + 1)
return neighbors
def count_total_swaps(self, gates: List[Tuple[str, List[int], List]]) -> int:
"""
Count total SWAPs needed for a circuit.
Args:
gates: List of (gate_type, qubits, params)
Returns:
Total number of SWAPs required
"""
total_swaps = 0
for gate_type, qubits, _ in gates:
if len(qubits) == 2:
swaps = self.synthesize_swap_network(qubits[0], qubits[1])
total_swaps += len(swaps)
return total_swaps
class ChiScheduler:
"""
Adaptive bond dimension scheduler for 2D circuits.
Adjusts χ based on circuit depth and entanglement structure.
"""
def __init__(self, config: MappingConfig):
self.config = config
def get_chi(self, layer: int, total_layers: int, gate_type: str, distance: int) -> int:
"""
Compute adaptive bond dimension for given layer.
Args:
layer: Current layer number
total_layers: Total circuit depth
gate_type: Type of gate being applied
distance: Distance between qubits in MPS
Returns:
Recommended bond dimension
"""
if self.config.chi_schedule == "fixed":
return 64
elif self.config.chi_schedule == "exponential":
# Exponential growth with depth
base_chi = 8
max_chi = 128
chi = int(base_chi * (1.5 ** (layer / 10)))
return min(chi, max_chi)
elif self.config.chi_schedule == "adaptive":
# Adaptive based on gate distance
base_chi = 16
# Long-range gates need higher χ
distance_factor = 1.0 + 0.2 * min(distance, 5)
# Entangling gates need higher χ
if gate_type in ["CNOT", "CZ", "SWAP"]:
entangle_factor = 1.5
else:
entangle_factor = 1.0
chi = int(base_chi * distance_factor * entangle_factor)
return min(chi, 256)
else:
return 64
class Planar2DCircuit:
"""
Main interface for 2D planar circuit simulation.
Handles:
- 2D layout specification
- Automatic snake mapping
- SWAP synthesis
- Adaptive χ scheduling
"""
def __init__(
self,
rows: int,
cols: int,
topology: Topology = Topology.SQUARE_GRID,
config: Optional[MappingConfig] = None,
):
"""
Initialize 2D planar circuit.
Args:
rows: Number of rows in grid
cols: Number of columns
topology: Qubit layout topology
config: Mapping configuration
"""
self.rows = rows
self.cols = cols
self.topology = topology
self.config = config or MappingConfig()
# Create layout
self.layout = self._create_layout()
# Create mapper
self.mapper = SnakeMapper(rows, cols)
# Create SWAP synthesizer
self.swap_synth = SWAPSynthesizer(self.layout, self.mapper)
# Create χ scheduler
self.chi_scheduler = ChiScheduler(self.config)
def _create_layout(self) -> Layout2D:
"""Create 2D layout based on topology"""
qubits = {}
coupling_map = []
# Create qubits
for r in range(self.rows):
for c in range(self.cols):
qubit = Qubit2D(row=r, col=c)
qubits[(r, c)] = qubit
# Create coupling map based on topology
if self.topology == Topology.SQUARE_GRID:
for r in range(self.rows):
for c in range(self.cols):
idx = r * self.cols + c
# Horizontal edges
if c < self.cols - 1:
neighbor_idx = r * self.cols + (c + 1)
coupling_map.append((idx, neighbor_idx))
# Vertical edges
if r < self.rows - 1:
neighbor_idx = (r + 1) * self.cols + c
coupling_map.append((idx, neighbor_idx))
return Layout2D(
rows=self.rows,
cols=self.cols,
topology=self.topology,
coupling_map=coupling_map,
qubits=qubits,
)
def compile_circuit(
self, gates_2d: List[Tuple[str, List[Tuple[int, int]], List]]
) -> List[Tuple[str, List[int], List]]:
"""
Compile 2D circuit to 1D MPS-compatible circuit with SWAPs.
Args:
gates_2d: Gates with (row, col) qubit specifications
Returns:
Gates with 1D qubit indices and inserted SWAPs
"""
gates_1d = []
for gate_type, qubits_2d, params in gates_2d:
# Map to 1D indices
qubits_1d = [self.mapper.map_2d_to_1d(r, c) for (r, c) in qubits_2d]
# For 2-qubit gates, insert SWAPs if needed
if len(qubits_1d) == 2:
control, target = qubits_1d
distance = self.mapper.get_distance(control, target)
if distance > 1 and self.config.optimize_swaps:
# Insert SWAP network
swaps = self.swap_synth.synthesize_swap_network(control, target)
for swap_pair in swaps:
gates_1d.append(("SWAP", list(swap_pair), []))
# Original gate (qubits now adjacent)
gates_1d.append((gate_type, qubits_1d, params))
# Reverse SWAPs to restore layout
for swap_pair in reversed(swaps):
gates_1d.append(("SWAP", list(swap_pair), []))
else:
gates_1d.append((gate_type, qubits_1d, params))
else:
# Single-qubit gate
gates_1d.append((gate_type, qubits_1d, params))
return gates_1d
def simulate(
self, gates_2d: List[Tuple[str, List[Tuple[int, int]], List]], device: str = "cuda"
):
"""
Simulate 2D circuit using MPS backend.
Args:
gates_2d: Circuit gates with 2D qubit coordinates
device: Torch device
Returns:
AdaptiveMPS final state
"""
from atlas_q.adaptive_mps import AdaptiveMPS
# Compile to 1D
gates_1d = self.compile_circuit(gates_2d)
# Create MPS
mps = AdaptiveMPS(
num_qubits=self.rows * self.cols,
bond_dim=self.chi_scheduler.get_chi(0, len(gates_1d), "H", 1),
device=device,
)
# Apply gates with adaptive χ
for layer, (gate_type, qubits, params) in enumerate(gates_1d):
# Get appropriate χ for this gate
if len(qubits) == 2:
distance = self.mapper.get_distance(qubits[0], qubits[1])
else:
distance = 0
chi = self.chi_scheduler.get_chi(layer, len(gates_1d), gate_type, distance)
# Apply gate (simplified - would use actual gate matrices)
# mps.apply_gate(gate_type, qubits, chi_max=chi)
pass # TODO: Implement full gate application
return mps
def visualize_layout(self, filename: Optional[str] = None):
"""
Visualize the 2D qubit layout and snake mapping.
Args:
filename: Optional file to save visualization
"""
try:
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Plot 2D layout
for r in range(self.rows):
for c in range(self.cols):
idx_1d = self.mapper.map_2d_to_1d(r, c)
ax1.scatter(c, -r, s=500, c="lightblue", edgecolors="black")
ax1.text(c, -r, str(idx_1d), ha="center", va="center", fontsize=12)
# Draw edges
for q1, q2 in self.layout.coupling_map:
r1, c1 = self.mapper.map_1d_to_2d(q1)
r2, c2 = self.mapper.map_1d_to_2d(q2)
ax1.plot([c1, c2], [-r1, -r2], "k-", alpha=0.3)
ax1.set_title("2D Qubit Layout (Snake Mapped)")
ax1.set_xlabel("Column")
ax1.set_ylabel("Row")
ax1.grid(True, alpha=0.3)
# Plot 1D MPS ordering
positions = list(range(self.rows * self.cols))
ax2.scatter(positions, [0] * len(positions), s=500, c="lightgreen", edgecolors="black")
for i, pos in enumerate(positions):
ax2.text(pos, 0, str(i), ha="center", va="center", fontsize=12)
# Draw MPS bonds
for i in range(len(positions) - 1):
ax2.plot([i, i + 1], [0, 0], "k-", linewidth=2)
ax2.set_title("1D MPS Ordering")
ax2.set_xlabel("MPS Index")
ax2.set_ylim(-0.5, 0.5)
ax2.grid(True, alpha=0.3, axis="x")
plt.tight_layout()
if filename:
plt.savefig(filename, dpi=150, bbox_inches="tight")
else:
plt.show()
except ImportError:
print("Matplotlib not available for visualization")
# Example usage
if __name__ == "__main__":
print("2D Planar Circuit Support Example")
print("=" * 50)
# Create 4×4 square grid
circuit_2d = Planar2DCircuit(rows=4, cols=4, topology=Topology.SQUARE_GRID)
print(f"Layout: {circuit_2d.rows}×{circuit_2d.cols} grid")
print(f"Total qubits: {circuit_2d.rows * circuit_2d.cols}")
print(f"Coupling map edges: {len(circuit_2d.layout.coupling_map)}")
# Example: map (row, col) to 1D
for r in range(4):
row_mapping = [circuit_2d.mapper.map_2d_to_1d(r, c) for c in range(4)]
print(f"Row {r}: {row_mapping}")
# Example gates in 2D coordinates
gates_2d = [
("H", [(0, 0)], []),
("H", [(0, 1)], []),
("CNOT", [(0, 0), (0, 1)], []), # Nearest neighbor
("CNOT", [(0, 0), (2, 2)], []), # Long range - needs SWAPs
]
# Compile to 1D
gates_1d = circuit_2d.compile_circuit(gates_2d)
print("\nCompiled circuit:")
print(f" Original gates: {len(gates_2d)}")
print(f" With SWAPs: {len(gates_1d)}")
# Count SWAPs
swap_count = sum(1 for g, _, _ in gates_1d if g == "SWAP")
print(f" Total SWAPs inserted: {swap_count}")