The QC simulator represents quantum states in the full joint Hilbert space C^(2^n), where each amplitude is encoded as a 2D spatial wavefunction on a (G×G) grid.
The Joint Hilbert Space
For n qubits, the quantum state lives in a 2^n-dimensional complex vector space. The simulator stores this as:
amplitudes: torch.Tensor # Shape: (2^n, 2, G, G)
Tensor Structure
Dimension 0: Computational basis index k ∈ {0, 1, ..., 2^n - 1}
Bit j of k represents the state of qubit j (MSB = qubit 0)
Dimension 1: Complex channel (0 = real, 1 = imaginary)
Dimension 2: Spatial x coordinate (G grid points, typically G=16)
Dimension 3: Spatial y coordinate (G grid points)
Example for n=2 (2 qubits):
amplitudes[0] → amplitude for |00⟩, shape (2, 16, 16)
amplitudes[1] → amplitude for |01⟩, shape (2, 16, 16)
amplitudes[2] → amplitude for |10⟩, shape (2, 16, 16)
amplitudes[3] → amplitude for |11⟩, shape (2, 16, 16)
Each amplitude α_k is a complex spatial field:
α_k(x, y) = amplitudes[k, 0, x, y] + i·amplitudes[k, 1, x, y]
JointHilbertState Class
The JointHilbertState class encapsulates the joint quantum state:
class JointHilbertState:
def __init__(self, amplitudes: torch.Tensor, n_qubits: int) -> None:
expected_dim0 = 2 ** n_qubits
if amplitudes.shape[0] != expected_dim0:
raise ValueError(
f"amplitudes.shape[0]={amplitudes.shape[0]} but 2^n_qubits={expected_dim0}"
)
self.amplitudes = amplitudes
self.n_qubits = n_qubits
self.dim = expected_dim0
self.device = amplitudes.device
self.G = amplitudes.shape[-1]
Key Properties
state.n_qubits # Number of qubits
state.dim # 2^n (Hilbert space dimension)
state.G # Grid size (typically 16)
state.device # torch device ("cpu" or "cuda")
state.amplitudes # The (2^n, 2, G, G) tensor
Born Probability Calculation
The probability of measuring computational basis state |k⟩ is computed by integrating the squared modulus over the spatial grid:
def probabilities(self) -> torch.Tensor:
"""Return (2^n,) tensor of Born probabilities P(k) for each basis state."""
probs = (self.amplitudes[:, 0] ** 2 + self.amplitudes[:, 1] ** 2).sum(dim=(-2, -1))
return probs / (probs.sum() + 1e-12)
Mathematical form:
P(k) = ∫∫ |α_k(x,y)|² dx dy
= Σ_{x,y} (α_k_real² + α_k_imag²)
Normalized so that Σ_k P(k) = 1.
Example: Bell State
For the Bell state (|00⟩ + |11⟩)/√2:
probs = state.probabilities()
print(probs)
# Output: tensor([0.5000, 0.0000, 0.0000, 0.5000])
# Interpretation: P(|00⟩) = 0.5, P(|11⟩) = 0.5
Normalization
The state is kept normalized so that Σ_k P(k) = 1:
def normalize_(self) -> None:
"""In-place normalization: sum_k P(k) = 1."""
probs = (self.amplitudes[:, 0] ** 2 + self.amplitudes[:, 1] ** 2).sum(dim=(-2, -1))
total = probs.sum() + 1e-12
self.amplitudes = self.amplitudes / (total ** 0.5)
Normalization is applied after:
- State initialization
- Gate application
- Time evolution
Marginal Probabilities
Single-Qubit Marginal
The probability that qubit j is in state |1⟩ is computed by summing over all basis states where bit j is set:
def marginal_probability_one(self, qubit: int) -> float:
"""P(qubit_j = |1⟩). Bit ordering: qubit 0 is MSB of k."""
probs = self.probabilities()
bit_pos = self.n_qubits - 1 - qubit
mask = torch.zeros(self.dim, dtype=torch.bool, device=self.device)
for k in range(self.dim):
if (k >> bit_pos) & 1:
mask[k] = True
return float(probs[mask].sum().clamp(0.0, 1.0))
Example: For a Bell state on qubits 0 and 1:
P(qubit 0 = |1⟩) = P(|10⟩) + P(|11⟩) = 0 + 0.5 = 0.5
P(qubit 1 = |1⟩) = P(|01⟩) + P(|11⟩) = 0 + 0.5 = 0.5
Bloch Vector (Reduced Density Matrix)
For single qubits, the Bloch vector is computed via partial trace:
def bloch_vector(self, qubit: int) -> Tuple[float, float, float]:
"""
Compute the reduced Bloch vector for qubit j by partial trace.
ρ_j[0,0] = P(qubit=0), ρ_j[1,1] = P(qubit=1)
ρ_j[0,1] = Σ_{pairs} α_{k0}^* α_{k1} (off-diagonal coherence)
bx = 2 Re(ρ_j[0,1]), by = -2 Im(ρ_j[0,1]), bz = P(0) - P(1)
"""
probs = self.probabilities()
bit_pos = self.n_qubits - 1 - qubit
p0 = sum(probs[k] for k in range(self.dim) if not ((k >> bit_pos) & 1))
p1 = 1.0 - p0
bz = p0 - p1
# Off-diagonal coherence terms
re_offdiag = 0.0
im_offdiag = 0.0
for k0 in range(self.dim):
if (k0 >> bit_pos) & 1:
continue
k1 = k0 | (1 << bit_pos)
a0r = self.amplitudes[k0, 0]
a0i = self.amplitudes[k0, 1]
a1r = self.amplitudes[k1, 0]
a1i = self.amplitudes[k1, 1]
# α_{k0}^* α_{k1}
re_offdiag += (a0r * a1r + a0i * a1i).sum()
im_offdiag += (a0r * a1i - a0i * a1r).sum()
total_weight = (self.amplitudes[:, 0] ** 2 + self.amplitudes[:, 1] ** 2).sum() + 1e-12
bx = 2.0 * re_offdiag / total_weight
by = -2.0 * im_offdiag / total_weight
# Normalize if magnitude > 1
mag = sqrt(bx**2 + by**2 + bz**2) + 1e-12
if mag > 1.0:
bx, by, bz = bx/mag, by/mag, bz/mag
return bx, by, bz
Interpretation:
bz = 1: qubit is in |0⟩
bz = -1: qubit is in |1⟩
bx = 1, by = 0, bz = 0: qubit is in |+⟩ = (|0⟩ + |1⟩)/√2
bx = 0, by = 0, bz = 0: maximally mixed state (entangled)
Most Probable State
def most_probable_basis_state(self) -> int:
"""Return the index k with the highest probability."""
return int(self.probabilities().argmax().item())
Usage:
k = state.most_probable_basis_state()
bitstring = format(k, f"0{state.n_qubits}b")
print(f"Most probable: |{bitstring}⟩")
# Example output: "Most probable: |101⟩"
State Cloning
def clone(self) -> "JointHilbertState":
"""Return a deep copy."""
return JointHilbertState(self.amplitudes.clone(), self.n_qubits)
Cloning is allowed because the simulator does not enforce the no-cloning theorem. This is used for debugging and circuit snapshots.
Amplitude Initialization
Amplitudes are initialized using eigenstates of mixed potential landscapes:
def _build_basis_amplitude(config: SimulatorConfig, basis_idx: int) -> torch.Tensor:
"""
Build the (2, G, G) spatial wavefunction for amplitude at basis index basis_idx.
Each computational basis state gets its own spatial eigenstate profile.
The excitation level is proportional to the popcount of the basis index.
"""
pot_gen = PotentialGenerator(config)
popcount = bin(basis_idx).count("1") # Number of |1⟩ bits
potential = pot_gen.mixed(seed=basis_idx * 17 + 3)
return _solve_eigenstate(config, potential, n=popcount)
The potential generator creates mixed landscapes from:
- Harmonic oscillator:
V ∝ r²
- Double well:
V ∝ (x² - 1)²
- Coulomb:
V ∝ -1/r
- Periodic lattice:
V ∝ cos(2x) + cos(2y)
Result: Each computational basis state |k⟩ has a unique spatial signature based on its bit pattern.
Superposition and Entanglement
Superposition
Single qubit in superposition:
|ψ⟩ = (|0⟩ + |1⟩)/√2
Representation:
amplitudes[0] → non-zero spatial field
amplitudes[1] → non-zero spatial field
P(0) = P(1) = 0.5
Entanglement
Bell state:
|Φ⁺⟩ = (|00⟩ + |11⟩)/√2
Representation:
amplitudes[0] → non-zero (for |00⟩)
amplitudes[1] → zero
amplitudes[2] → zero
amplitudes[3] → non-zero (for |11⟩)
This CANNOT be factorized as |ψ_0⟩ ⊗ |ψ_1⟩
Evidence of entanglement:
- Bloch vectors for individual qubits:
(0, 0, 0) (maximally mixed)
- Shannon entropy: 1.0 bit (maximal for 2-state distribution)
- Off-diagonal coherence between
|00⟩ and |11⟩ is non-zero
Why This Representation?
The (2^n, 2, G, G) representation enables:
- Exact entanglement: Amplitudes do not factorize across qubits
- Spatial physics: Each amplitude evolves under continuous Hamiltonian dynamics
- Neural backend training: 2D spatial fields are natural inputs for convolutional networks
- Coherent multi-qubit gates: Exact permutation and mixing of amplitude pairs
This representation differs from tensor network methods (MPS, PEPS) which compress the exponential dimension. Here, we explicitly store all 2^n amplitudes, trading memory for exact dynamics.
State Factory
The JointStateFactory provides convenience methods for creating initial states:
class JointStateFactory:
def all_zeros(self, n_qubits: int) -> JointHilbertState:
"""Initialize register in |00...0⟩."""
amps = torch.zeros(2**n_qubits, 2, self.config.grid_size,
self.config.grid_size, device=self.config.device)
amps[0] = _build_basis_amplitude(self.config, 0)
state = JointHilbertState(amps, n_qubits)
state.normalize_()
return state
def basis_state(self, n_qubits: int, k: int) -> JointHilbertState:
"""Initialize register in computational basis state |k⟩."""
amps = torch.zeros(2**n_qubits, 2, self.config.grid_size,
self.config.grid_size, device=self.config.device)
amps[k] = _build_basis_amplitude(self.config, k)
state = JointHilbertState(amps, n_qubits)
state.normalize_()
return state
def from_bitstring(self, bitstring: str) -> JointHilbertState:
"""Initialize in the basis state given by binary string."""
return self.basis_state(len(bitstring), int(bitstring, 2))
Usage:
factory = JointStateFactory(config)
# Create |000⟩
state = factory.all_zeros(3)
# Create |101⟩
state = factory.basis_state(3, 5) # 5 = 0b101
# From bitstring
state = factory.from_bitstring("101")
See Also