Overview
The JointHilbertState class represents the quantum state of n qubits in the full joint Hilbert space (dimension 2^n). Unlike “statevector” approaches that store n separate qubit states, this representation stores the complete set of 2^n complex amplitudes, enabling true entanglement.
Key Features:
- Complete 2^n amplitude representation
- Each amplitude is a spatial wavefunction (2D grid)
- Supports superposition, entanglement, and multi-qubit coherence
- Non-destructive Born-rule measurements
- Marginal probabilities and Bloch vector calculations
Location: quantum_computer.py:277-386
State Tensor Structure
The state is stored as a tensor with shape (2^n, 2, G, G):
- Dimension 0 (size 2^n): Computational basis index k ∈ {0, …, 2^n - 1}
- Bit j of k represents the state of qubit j (MSB = qubit 0)
- Example: For n=3, k=5 (binary 101) represents |101⟩
- Dimension 1 (size 2): Real and imaginary channels
- Channel 0: real part of amplitude
- Channel 1: imaginary part of amplitude
- Dimensions 2, 3 (size G × G): Spatial wavefunction grid
- G = grid_size (typically 16 or 32)
- Each amplitude is a 2D spatial field
Born Probability
The probability of measuring basis state |k⟩ is:
P(k) = ∫∫ |α_k(x,y)|² dx dy
= Σ_{x,y} (α_k_real² + α_k_imag²)
normalized so that Σ_k P(k) = 1.
Constructor
JointHilbertState(amplitudes: torch.Tensor, n_qubits: int)
Create a joint Hilbert state from amplitude tensor.
Amplitude tensor of shape (2^n_qubits, 2, G, G). The tensor must be on the correct device (CPU/CUDA) and have the expected dimensions.
Number of qubits. Must satisfy amplitudes.shape[0] == 2^n_qubits.
Example
import torch
from quantum_computer import JointHilbertState
n_qubits = 2
G = 16
# Create amplitude tensor (2^2=4 basis states, 2 channels, 16×16 grid)
amplitudes = torch.zeros(4, 2, G, G)
amplitudes[0, 0] = 1.0 # |00⟩ with amplitude 1 (real part)
state = JointHilbertState(amplitudes, n_qubits)
print(f"State dimension: {state.dim}") # 4
print(f"Grid size: {state.G}") # 16
Direct construction is rare. Use JointStateFactory to create initial states or let QuantumComputer.run() manage state evolution.
Attributes
amplitudes
The amplitude tensor of shape (2^n, 2, G, G).
n_qubits
Number of qubits in the register.
dim
Hilbert space dimension (2^n_qubits).
device
Device where amplitudes are stored (CPU or CUDA).
Spatial grid size (amplitudes.shape[-1]).
Methods
normalize_
In-place normalization ensuring Σ_k P(k) = 1.
probabilities
probabilities() -> torch.Tensor
Return Born probabilities P(k) for each basis state.
Tensor of shape (2^n,) containing probabilities for basis states k=0, 1, …, 2^n - 1.
Example
probs = state.probabilities()
print(probs) # tensor([0.5, 0.0, 0.0, 0.5]) # Bell state |00⟩ + |11⟩
marginal_probability_one
marginal_probability_one(qubit: int) -> float
Compute marginal Born probability P(qubit_j = |1⟩).
Sums P(k) over all basis states k where bit j equals 1.
Probability that qubit j measures |1⟩, clamped to [0, 1]
Example
# Bell state: equal superposition of |00⟩ and |11⟩
p1_q0 = state.marginal_probability_one(0) # 0.5
p1_q1 = state.marginal_probability_one(1) # 0.5
most_probable_basis_state
most_probable_basis_state() -> int
Return the basis state index k with the highest probability.
Basis state index (0 to 2^n - 1) with maximum P(k)
Example
k = state.most_probable_basis_state()
bitstring = format(k, f"0{state.n_qubits}b")
print(f"Most probable: |{bitstring}⟩") # |00⟩
bloch_vector
bloch_vector(qubit: int) -> Tuple[float, float, float]
Compute the reduced Bloch vector (bx, by, bz) for qubit j via partial trace.
The reduced density matrix for qubit j is:
ρ_j[0,0] = P(qubit=0)
ρ_j[1,1] = P(qubit=1)
ρ_j[0,1] = Σ_{pairs} α_{k0}* α_{k1} (off-diagonal coherence)
Bloch vector components:
bx = 2 Re(ρ_j[0,1])
by = -2 Im(ρ_j[0,1])
bz = P(0) - P(1)
Normalized to unit length if |b| > 1.
bloch
Tuple[float, float, float]
Bloch vector (bx, by, bz) with |b| ≤ 1
Example
bx, by, bz = state.bloch_vector(0)
print(f"Bloch vector: ({bx:+.3f}, {by:+.3f}, {bz:+.3f})")
# Pure |0⟩: (0.000, 0.000, +1.000)
# Pure |1⟩: (0.000, 0.000, -1.000)
# Equal superposition: (varies, varies, 0.000)
clone
clone() -> JointHilbertState
Return a deep copy of the state.
New JointHilbertState with independent amplitude tensor
state_copy = state.clone()
# Modifications to state_copy don't affect state
Factory: JointStateFactory
The JointStateFactory class provides convenient methods to create initial states.
Constructor
JointStateFactory(config: SimulatorConfig)
Simulator configuration (grid size, device, etc.)
all_zeros
all_zeros(n_qubits: int) -> JointHilbertState
Initialize register in |00…0⟩.
Normalized state with P(|0…0⟩) = 1
Example
from quantum_computer import SimulatorConfig, JointStateFactory
config = SimulatorConfig(grid_size=16, device="cuda")
factory = JointStateFactory(config)
state = factory.all_zeros(3)
print(state.probabilities()) # tensor([1., 0., 0., 0., 0., 0., 0., 0.])
basis_state
basis_state(n_qubits: int, k: int) -> JointHilbertState
Initialize register in computational basis state |k⟩.
Basis state index (0 to 2^n_qubits - 1)
Normalized state with P(|k⟩) = 1
Example
# Create |101⟩ (k=5 for 3 qubits)
state = factory.basis_state(3, 5)
print(state.probabilities()) # tensor([0., 0., 0., 0., 0., 1., 0., 0.])
from_bitstring
from_bitstring(bitstring: str) -> JointHilbertState
Initialize in basis state given by binary string.
Binary string (e.g., “101”)
Normalized state corresponding to bitstring
Example
state = factory.from_bitstring("101")
print(state.n_qubits) # 3
print(state.probabilities()[5]) # 1.0 (k=5 for "101")
Complete Examples
Accessing Amplitude Data
from quantum_computer import QuantumComputer, QuantumCircuit
qc = QuantumComputer()
# Build Bell state circuit
circuit = QuantumCircuit(2)
circuit.h(0).cnot(0, 1)
# Get state via run_with_state_snapshots
final, _ = qc.run_with_state_snapshots(circuit, snapshot_after=[])
# Access underlying state (not exposed in public API)
# In practice, use MeasurementResult methods instead
Measuring Entanglement
import math
def is_entangled(state: JointHilbertState, tol: float = 0.01) -> bool:
"""Check if state is entangled by measuring correlation."""
probs = state.probabilities().cpu().numpy()
n = state.n_qubits
# For Bell state: P(|00⟩) and P(|11⟩) should be ~0.5 each
# For product state: probabilities factorize
# Simple heuristic: max probability < 0.9 indicates superposition
max_prob = probs.max()
return max_prob < (1.0 - tol)
# Example
qc = QuantumComputer()
# Product state |01⟩
c1 = QuantumCircuit(2)
c1.x(1)
r1 = qc.run(c1)
print(is_entangled(r1)) # False (not accessible via public API)
# Bell state
c2 = QuantumCircuit(2)
c2.h(0).cnot(0, 1)
r2 = qc.run(c2)
print(is_entangled(r2)) # True
Bloch Sphere Visualization
def print_bloch_vectors(state: JointHilbertState) -> None:
"""Print Bloch vectors for all qubits."""
print(f"Bloch vectors for {state.n_qubits}-qubit state:")
for i in range(state.n_qubits):
bx, by, bz = state.bloch_vector(i)
magnitude = math.sqrt(bx**2 + by**2 + bz**2)
print(f" q{i}: ({bx:+.3f}, {by:+.3f}, {bz:+.3f}) |b|={magnitude:.3f}")
# Example: GHZ state
qc = QuantumComputer()
circuit = QuantumCircuit(3)
circuit.h(0).cnot(0, 1).cnot(1, 2)
result = qc.run(circuit)
# Use result.bloch_vectors instead of direct state access
for i in range(3):
bx, by, bz = result.bloch_vectors[i]
print(f"q{i}: Bloch=({bx:+.3f}, {by:+.3f}, {bz:+.3f})")
Marginal Probability Distribution
def marginal_distribution(state: JointHilbertState, qubit: int) -> dict:
"""Compute marginal distribution for a single qubit."""
p0 = 1.0 - state.marginal_probability_one(qubit)
p1 = state.marginal_probability_one(qubit)
return {"0": p0, "1": p1}
# Example
qc = QuantumComputer()
circuit = QuantumCircuit(2)
circuit.h(0) # Only qubit 0 in superposition
result = qc.run(circuit)
# Access via MeasurementResult API
print(f"q0 marginal: P(0)={1 - result.marginal_p1[0]:.3f}, P(1)={result.marginal_p1[0]:.3f}")
print(f"q1 marginal: P(0)={1 - result.marginal_p1[1]:.3f}, P(1)={result.marginal_p1[1]:.3f}")
# Expected: q0 is 50/50, q1 is 100% |0⟩
Understanding Entanglement
The joint Hilbert space representation is the only correct way to represent entangled states:
Product State (Not Entangled)
# |0⟩ ⊗ |1⟩ = |01⟩
# Only amplitude[1] is non-zero (k=1 = binary 01)
state = factory.from_bitstring("01")
print(state.probabilities()) # [0, 1, 0, 0]
Bell State (Maximally Entangled)
# (|00⟩ + |11⟩) / √2
# Amplitudes[0] and amplitudes[3] are non-zero
# CANNOT be written as q0 ⊗ q1
circuit = QuantumCircuit(2)
circuit.h(0).cnot(0, 1)
result = qc.run(circuit)
print(result.full_distribution) # {"00": ~0.5, "11": ~0.5}
GHZ State (Multi-qubit Entanglement)
# (|000⟩ + |111⟩) / √2
# Only amplitudes[0] and amplitudes[7] are non-zero
circuit = QuantumCircuit(3)
circuit.h(0).cnot(0, 1).cnot(1, 2)
result = qc.run(circuit)
print(result.full_distribution) # {"000": ~0.5, "111": ~0.5}
Bit Ordering Convention
Important: Qubit 0 is the most significant bit (MSB) of the basis index k.
For a 3-qubit state:
- k = 0 (binary 000) = |q0=0, q1=0, q2=0⟩
- k = 1 (binary 001) = |q0=0, q1=0, q2=1⟩
- k = 5 (binary 101) = |q0=1, q1=0, q2=1⟩
- k = 7 (binary 111) = |q0=1, q1=1, q2=1⟩
def basis_to_qubits(k: int, n_qubits: int) -> str:
"""Convert basis index k to qubit string."""
return format(k, f"0{n_qubits}b")
print(basis_to_qubits(5, 3)) # "101" = |q0=1, q1=0, q2=1⟩
See Also