Deminiko
Initial commit: QuantumArchitect-MCP quantum circuit MCP server with Gradio UI
6ce350d
"""
Statevector Simulator - Lightweight ideal simulator for quantum circuits.
Calculates the final probability vector without noise.
"""
from typing import Any
import numpy as np
from ..creation.gate_library import GateLibrary
def simulate_statevector(
circuit_data: dict[str, Any],
initial_state: list[complex] | None = None
) -> dict[str, Any]:
"""
Simulate a quantum circuit and return the final statevector.
Args:
circuit_data: Circuit dictionary with gates
initial_state: Optional initial state (default: |0...0⟩)
Returns:
Simulation results with statevector and probabilities
"""
num_qubits = circuit_data.get("num_qubits", 0)
gates = circuit_data.get("gates", [])
if num_qubits > 20:
return {
"success": False,
"error": f"Circuit too large for statevector simulation ({num_qubits} qubits). "
f"Maximum supported: 20 qubits (2^20 = 1M amplitudes)."
}
dim = 2 ** num_qubits
# Initialize state
if initial_state:
statevector = np.array(initial_state, dtype=complex)
if len(statevector) != dim:
return {
"success": False,
"error": f"Initial state dimension ({len(statevector)}) doesn't match "
f"circuit dimension ({dim})"
}
else:
# Start in |0...0⟩
statevector = np.zeros(dim, dtype=complex)
statevector[0] = 1.0
# Apply gates
measurement_results: dict[int, int] = {} # qubit -> classical bit value
gates_applied = 0
for gate in gates:
name = gate.get("name", "").lower()
qubits = gate.get("qubits", [])
params = gate.get("params", [])
if name == "barrier":
continue
if name == "measure":
# Simulate measurement
for q in qubits:
prob_0, prob_1 = _get_qubit_probabilities(statevector, q, num_qubits)
# Deterministically collapse to most likely state for reproducibility
result = 0 if prob_0 >= prob_1 else 1
measurement_results[q] = result
statevector = _collapse_state(statevector, q, result, num_qubits)
continue
if name == "reset":
for q in qubits:
statevector = _reset_qubit(statevector, q, num_qubits)
continue
# Skip symbolic parameters
if any(isinstance(p, str) and p.startswith("param:") for p in params):
continue
try:
gate_matrix = GateLibrary.get_gate(name, params if params else None)
statevector = _apply_gate(statevector, gate_matrix, qubits, num_qubits)
gates_applied += 1
except Exception as e:
return {
"success": False,
"error": f"Failed to apply gate '{name}': {str(e)}"
}
# Calculate probabilities
probabilities = np.abs(statevector) ** 2
# Get most likely outcomes
sorted_indices = np.argsort(probabilities)[::-1]
top_outcomes = []
for idx in sorted_indices[:min(16, len(probabilities))]:
if probabilities[idx] > 1e-10:
bitstring = format(idx, f'0{num_qubits}b')
top_outcomes.append({
"state": f"|{bitstring}⟩",
"probability": float(probabilities[idx]),
"amplitude": {
"real": float(statevector[idx].real),
"imag": float(statevector[idx].imag)
}
})
return {
"success": True,
"num_qubits": num_qubits,
"gates_applied": gates_applied,
"statevector_dimension": dim,
"top_outcomes": top_outcomes,
"total_probability": float(np.sum(probabilities)),
"measurement_results": measurement_results,
"statevector": {
"real": statevector.real.tolist(),
"imag": statevector.imag.tolist()
} if num_qubits <= 6 else None, # Only include full statevector for small circuits
}
def _apply_gate(
statevector: np.ndarray,
gate_matrix: np.ndarray,
target_qubits: list[int],
num_qubits: int
) -> np.ndarray:
"""Apply a gate to specific qubits in the statevector."""
dim = 2 ** num_qubits
if len(target_qubits) == 1:
q = target_qubits[0]
new_state = np.zeros_like(statevector)
for i in range(dim):
# Get bit value at position q
bit_q = (i >> q) & 1
# Calculate partner index (flip bit q)
partner = i ^ (1 << q)
if bit_q == 0:
# Apply gate to [amplitude_0, amplitude_1]
amp_0 = statevector[i]
amp_1 = statevector[partner]
new_amp_0 = gate_matrix[0, 0] * amp_0 + gate_matrix[0, 1] * amp_1
new_amp_1 = gate_matrix[1, 0] * amp_0 + gate_matrix[1, 1] * amp_1
new_state[i] = new_amp_0
new_state[partner] = new_amp_1
return new_state
elif len(target_qubits) == 2:
q0, q1 = target_qubits
new_state = np.zeros_like(statevector)
processed = set()
for i in range(dim):
if i in processed:
continue
# Get all 4 basis states for this 2-qubit subspace
bit_q0 = (i >> q0) & 1
bit_q1 = (i >> q1) & 1
# Base index (both qubits = 0)
base = i & ~(1 << q0) & ~(1 << q1)
# Four indices in gate matrix order |control,target⟩:
# |00⟩, |01⟩ (target=1), |10⟩ (control=1), |11⟩
# For CX(control=q0, target=q1), the gate matrix expects this ordering
indices = [
base, # |00⟩
base | (1 << q1), # |01⟩ - target set
base | (1 << q0), # |10⟩ - control set
base | (1 << q0) | (1 << q1) # |11⟩
]
# Extract amplitudes
amps = np.array([statevector[idx] for idx in indices], dtype=complex)
# Apply gate
new_amps = gate_matrix @ amps
# Write back
for j, idx in enumerate(indices):
new_state[idx] = new_amps[j]
processed.add(idx)
return new_state
else:
# For 3+ qubit gates, use general approach
# This is computationally expensive but correct
return _apply_multiqubit_gate(statevector, gate_matrix, target_qubits, num_qubits)
def _apply_multiqubit_gate(
statevector: np.ndarray,
gate_matrix: np.ndarray,
target_qubits: list[int],
num_qubits: int
) -> np.ndarray:
"""Apply a multi-qubit gate using full matrix expansion."""
# Build full gate matrix by tensor products
n = len(target_qubits)
dim = 2 ** num_qubits
# For simplicity, use matrix-vector multiplication with full expansion
# This is O(4^n) but works for small circuits
full_gate = np.eye(dim, dtype=complex)
# This is a simplified implementation
# A proper implementation would use efficient tensor contractions
return full_gate @ statevector
def _get_qubit_probabilities(
statevector: np.ndarray,
qubit: int,
num_qubits: int
) -> tuple[float, float]:
"""Get probability of measuring 0 or 1 on a specific qubit."""
dim = 2 ** num_qubits
prob_0 = 0.0
prob_1 = 0.0
for i in range(dim):
amplitude_sq = abs(statevector[i]) ** 2
if (i >> qubit) & 1:
prob_1 += amplitude_sq
else:
prob_0 += amplitude_sq
return prob_0, prob_1
def _collapse_state(
statevector: np.ndarray,
qubit: int,
result: int,
num_qubits: int
) -> np.ndarray:
"""Collapse statevector after measurement."""
dim = 2 ** num_qubits
new_state = np.zeros_like(statevector)
for i in range(dim):
if ((i >> qubit) & 1) == result:
new_state[i] = statevector[i]
# Renormalize
norm = np.linalg.norm(new_state)
if norm > 0:
new_state /= norm
return new_state
def _reset_qubit(
statevector: np.ndarray,
qubit: int,
num_qubits: int
) -> np.ndarray:
"""Reset a qubit to |0⟩."""
dim = 2 ** num_qubits
new_state = np.zeros_like(statevector)
for i in range(dim):
bit_value = (i >> qubit) & 1
target_idx = i & ~(1 << qubit) # Set qubit to 0
new_state[target_idx] += statevector[i]
# Renormalize
norm = np.linalg.norm(new_state)
if norm > 0:
new_state /= norm
return new_state
def sample_circuit(
circuit_data: dict[str, Any],
shots: int = 1024
) -> dict[str, Any]:
"""
Sample measurement outcomes from a circuit.
Args:
circuit_data: Circuit dictionary
shots: Number of samples
Returns:
Histogram of measurement outcomes
"""
# First get the statevector
sim_result = simulate_statevector(circuit_data)
if not sim_result.get("success", False):
return sim_result
num_qubits = sim_result["num_qubits"]
dim = 2 ** num_qubits
# Get probabilities from statevector
sv = sim_result.get("statevector", {})
if sv:
real = np.array(sv["real"])
imag = np.array(sv["imag"])
statevector = real + 1j * imag
probabilities = np.abs(statevector) ** 2
else:
# Reconstruct from top outcomes
probabilities = np.zeros(dim)
for outcome in sim_result.get("top_outcomes", []):
state = outcome["state"]
bitstring = state[1:-1] # Remove |⟩
idx = int(bitstring, 2)
probabilities[idx] = outcome["probability"]
# Sample
indices = np.random.choice(dim, size=shots, p=probabilities)
counts: dict[str, int] = {}
for idx in indices:
bitstring = format(idx, f'0{num_qubits}b')
counts[bitstring] = counts.get(bitstring, 0) + 1
# Sort by count
sorted_counts = dict(sorted(counts.items(), key=lambda x: -x[1]))
# Build probabilities dict with bitstring keys
prob_dict: dict[str, float] = {}
for idx in range(dim):
prob = float(probabilities[idx])
if prob > 1e-10: # Only include non-zero probabilities
bitstring = format(idx, f'0{num_qubits}b')
prob_dict[bitstring] = prob
return {
"success": True,
"shots": shots,
"counts": sorted_counts,
"probabilities": prob_dict,
"most_frequent": list(sorted_counts.keys())[0] if sorted_counts else None,
}