Spaces:
Running
Running
| """ | |
| 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, | |
| } | |