Spaces:
Running
Running
| """ | |
| Quantum Circuit Designer | |
| This module handles the creation, validation, simulation, and storage of custom quantum circuits. | |
| It leverages Cirq for the quantum operations and simulation, with a Flask API interface. | |
| """ | |
| import cirq | |
| import numpy as np | |
| import json | |
| import uuid | |
| import os | |
| from flask import jsonify, request, abort | |
| from typing import Dict, List, Optional, Tuple, Any, Union | |
| # Circuit storage directory | |
| CIRCUIT_DIR = os.path.join(os.path.dirname(__file__), 'user_circuits') | |
| os.makedirs(CIRCUIT_DIR, exist_ok=True) | |
| class QuantumCircuitDesigner: | |
| """Handles creation and simulation of custom quantum circuits""" | |
| # Available gates and their parameters - store string identifiers instead of actual gate objects | |
| SINGLE_QUBIT_GATES = { | |
| 'H': {'name': 'Hadamard', 'cirq_gate_id': 'cirq.H'}, | |
| 'X': {'name': 'Pauli-X', 'cirq_gate_id': 'cirq.X'}, | |
| 'Y': {'name': 'Pauli-Y', 'cirq_gate_id': 'cirq.Y'}, | |
| 'Z': {'name': 'Pauli-Z', 'cirq_gate_id': 'cirq.Z'}, | |
| 'S': {'name': 'Phase Gate', 'cirq_gate_id': 'cirq.S'}, | |
| 'T': {'name': 'T Gate', 'cirq_gate_id': 'cirq.T'}, | |
| 'Rx': {'name': 'Rotation-X', 'cirq_gate_id': 'cirq.rx', 'params': ['angle']}, | |
| 'Ry': {'name': 'Rotation-Y', 'cirq_gate_id': 'cirq.ry', 'params': ['angle']}, | |
| 'Rz': {'name': 'Rotation-Z', 'cirq_gate_id': 'cirq.rz', 'params': ['angle']} | |
| } | |
| MULTI_QUBIT_GATES = { | |
| 'CNOT': {'name': 'Controlled-NOT', 'cirq_gate_id': 'cirq.CNOT', 'num_qubits': 2}, | |
| 'CZ': {'name': 'Controlled-Z', 'cirq_gate_id': 'cirq.CZ', 'num_qubits': 2}, | |
| 'SWAP': {'name': 'SWAP', 'cirq_gate_id': 'cirq.SWAP', 'num_qubits': 2}, | |
| 'CSWAP': {'name': 'Controlled-SWAP', 'cirq_gate_id': 'cirq.CSWAP', 'num_qubits': 3}, | |
| 'TOFFOLI': {'name': 'Toffoli', 'cirq_gate_id': 'cirq.TOFFOLI', 'num_qubits': 3} | |
| } | |
| def __init__(self): | |
| """Initialize the circuit designer""" | |
| self.reset_circuit() | |
| def reset_circuit(self) -> None: | |
| """Reset the current circuit""" | |
| self.qubits = [] | |
| self.circuit = cirq.Circuit() | |
| self.circuit_data = { | |
| 'id': str(uuid.uuid4()), | |
| 'name': 'New Circuit', | |
| 'num_qubits': 0, | |
| 'depth': 0, | |
| 'gates': [] | |
| } | |
| def create_empty_circuit(self, num_qubits: int, name: str = 'New Circuit') -> Dict[str, Any]: | |
| """Create a new empty circuit with the specified number of qubits""" | |
| if num_qubits <= 0 or num_qubits > 20: # Reasonable limits | |
| raise ValueError("Number of qubits must be between 1 and 20") | |
| self.reset_circuit() | |
| self.qubits = [cirq.NamedQubit(f'q{i}') for i in range(num_qubits)] | |
| self.circuit_data = { | |
| 'id': str(uuid.uuid4()), | |
| 'name': name, | |
| 'num_qubits': num_qubits, | |
| 'depth': 0, | |
| 'gates': [] | |
| } | |
| return self.circuit_data | |
| def add_gate(self, gate_type: str, targets: List[int], params: Optional[Dict[str, float]] = None) -> Dict[str, Any]: | |
| """ | |
| Add a gate to the circuit | |
| Args: | |
| gate_type: Type of gate (e.g., 'H', 'CNOT') | |
| targets: List of target qubit indices | |
| params: Optional parameters for parametrized gates | |
| Returns: | |
| Updated circuit data | |
| """ | |
| if not self.qubits: | |
| raise ValueError("No circuit initialized. Call create_empty_circuit first.") | |
| # Ensure targets are integers | |
| targets = [int(t) for t in targets] | |
| # Handle single-qubit gates | |
| if gate_type in self.SINGLE_QUBIT_GATES: | |
| if len(targets) != 1: | |
| raise ValueError(f"{gate_type} gate requires exactly 1 target qubit") | |
| gate_info = self.SINGLE_QUBIT_GATES[gate_type] | |
| target_qubit = self.qubits[targets[0]] | |
| # Handle parameterized gates | |
| if 'params' in gate_info: | |
| if not params: | |
| raise ValueError(f"{gate_type} gate requires parameters: {gate_info['params']}") | |
| # Create the appropriate parameterized gate | |
| if gate_type == 'Rx': | |
| gate = cirq.rx(rads=params.get('angle', 0)) | |
| elif gate_type == 'Ry': | |
| gate = cirq.ry(rads=params.get('angle', 0)) | |
| elif gate_type == 'Rz': | |
| gate = cirq.rz(rads=params.get('angle', 0)) | |
| self.circuit.append(gate(target_qubit)) | |
| else: | |
| # Simple gate with no parameters - use appropriate Cirq gate based on type | |
| if gate_type == 'H': | |
| self.circuit.append(cirq.H(target_qubit)) | |
| elif gate_type == 'X': | |
| self.circuit.append(cirq.X(target_qubit)) | |
| elif gate_type == 'Y': | |
| self.circuit.append(cirq.Y(target_qubit)) | |
| elif gate_type == 'Z': | |
| self.circuit.append(cirq.Z(target_qubit)) | |
| elif gate_type == 'S': | |
| self.circuit.append(cirq.S(target_qubit)) | |
| elif gate_type == 'T': | |
| self.circuit.append(cirq.T(target_qubit)) | |
| # Handle multi-qubit gates | |
| elif gate_type in self.MULTI_QUBIT_GATES: | |
| gate_info = self.MULTI_QUBIT_GATES[gate_type] | |
| required_qubits = gate_info['num_qubits'] | |
| if len(targets) != required_qubits: | |
| raise ValueError(f"{gate_type} gate requires exactly {required_qubits} target qubits") | |
| # Get target qubits | |
| target_qubits = [self.qubits[i] for i in targets] | |
| # Apply the appropriate gate based on type | |
| if gate_type == 'CNOT': | |
| self.circuit.append(cirq.CNOT(*target_qubits)) | |
| elif gate_type == 'CZ': | |
| self.circuit.append(cirq.CZ(*target_qubits)) | |
| elif gate_type == 'SWAP': | |
| self.circuit.append(cirq.SWAP(*target_qubits)) | |
| elif gate_type == 'CSWAP': | |
| self.circuit.append(cirq.CSWAP(*target_qubits)) | |
| elif gate_type == 'TOFFOLI': | |
| self.circuit.append(cirq.TOFFOLI(*target_qubits)) | |
| # Handle measurement | |
| elif gate_type == 'MEASURE': | |
| if len(targets) != 1: | |
| raise ValueError("MEASURE requires exactly 1 target qubit") | |
| target_qubit = self.qubits[targets[0]] | |
| self.circuit.append(cirq.measure(target_qubit, key=f'q{targets[0]}')) | |
| else: | |
| raise ValueError(f"Unknown gate type: {gate_type}") | |
| # Update circuit data (same as before) | |
| gate_data = { | |
| 'type': gate_type, | |
| 'targets': targets, | |
| } | |
| if params: | |
| gate_data['params'] = params | |
| self.circuit_data['gates'].append(gate_data) | |
| # Update circuit depth (simplified approach) | |
| self.circuit_data['depth'] = len(self.circuit_data['gates']) | |
| return self.circuit_data | |
| def remove_gate(self, gate_index: int) -> Dict[str, Any]: | |
| """ | |
| Remove a gate from the circuit by its index | |
| Args: | |
| gate_index: Index of the gate to remove | |
| Returns: | |
| Updated circuit data | |
| """ | |
| if gate_index < 0 or gate_index >= len(self.circuit_data['gates']): | |
| raise ValueError(f"Gate index out of range: {gate_index}") | |
| # Remove the gate from circuit data | |
| self.circuit_data['gates'].pop(gate_index) | |
| # Rebuild the circuit from scratch | |
| self.circuit = cirq.Circuit() | |
| for gate_data in self.circuit_data['gates']: | |
| gate_type = gate_data['type'] | |
| targets = gate_data['targets'] | |
| params = gate_data.get('params') | |
| # Reapply gates (simplified - would need to match logic in add_gate) | |
| if gate_type in self.SINGLE_QUBIT_GATES: | |
| gate_info = self.SINGLE_QUBIT_GATES[gate_type] | |
| target_qubit = self.qubits[targets[0]] | |
| if 'params' in gate_info: | |
| # Parameterized gates | |
| angle = params.get('angle', 0) if params else 0 | |
| if gate_type == 'Rx': | |
| gate = cirq.rx(rads=angle) | |
| elif gate_type == 'Ry': | |
| gate = cirq.ry(rads=angle) | |
| elif gate_type == 'Rz': | |
| gate = cirq.rz(rads=angle) | |
| self.circuit.append(gate(target_qubit)) | |
| else: | |
| # Simple gates | |
| self.circuit.append(gate_info['cirq_gate'](target_qubit)) | |
| elif gate_type in self.MULTI_QUBIT_GATES: | |
| gate_info = self.MULTI_QUBIT_GATES[gate_type] | |
| target_qubits = [self.qubits[i] for i in targets] | |
| self.circuit.append(gate_info['cirq_gate'](*target_qubits)) | |
| elif gate_type == 'MEASURE': | |
| target_qubit = self.qubits[targets[0]] | |
| self.circuit.append(cirq.measure(target_qubit, key=f'q{targets[0]}')) | |
| # Update circuit depth | |
| self.circuit_data['depth'] = len(self.circuit_data['gates']) | |
| return self.circuit_data | |
| def simulate_circuit(self, shots: int = 1024) -> Dict[str, Any]: | |
| """ | |
| Simulate the current circuit | |
| Args: | |
| shots: Number of measurement shots for statistics | |
| Returns: | |
| Simulation results including statevector and measurements | |
| """ | |
| if not self.circuit.has_measurements(): | |
| # If there are no measurements, add them to all qubits for statistics | |
| measure_circuit = cirq.Circuit() | |
| for i, qubit in enumerate(self.qubits): | |
| measure_circuit.append(cirq.measure(qubit, key=f'q{i}')) | |
| sim_circuit = self.circuit + measure_circuit | |
| else: | |
| sim_circuit = self.circuit | |
| # Run simulation without measurements to get statevector | |
| simulator = cirq.Simulator() | |
| try: | |
| # First, get the statevector | |
| no_measure_circuit = cirq.Circuit() | |
| for op in self.circuit.all_operations(): | |
| if not isinstance(op.gate, cirq.MeasurementGate): | |
| no_measure_circuit.append(op) | |
| result = simulator.simulate(no_measure_circuit) | |
| statevector = result.final_state_vector | |
| # Calculate probabilities | |
| num_qubits = len(self.qubits) | |
| probabilities = np.abs(statevector)**2 | |
| # Format statevector data | |
| state_data = [] | |
| for i, amplitude in enumerate(statevector): | |
| if abs(probabilities[i]) > 1e-10: # Threshold for numerical precision | |
| state_data.append({ | |
| 'state': format(i, f'0{num_qubits}b'), | |
| 'amplitude': { | |
| 'real': float(amplitude.real), | |
| 'imag': float(amplitude.imag) | |
| }, | |
| 'probability': float(probabilities[i]) | |
| }) | |
| # Now run with measurements to get statistics | |
| sampler = cirq.Simulator() | |
| result = sampler.run(sim_circuit, repetitions=shots) | |
| # Process measurement results | |
| measurement_keys = sorted(result.measurements.keys()) | |
| # Convert to bitstring representation | |
| counts = {} | |
| for i in range(shots): | |
| bitstring = ''.join([str(int(result.measurements[key][i][0])) for key in measurement_keys]) | |
| counts[bitstring] = counts.get(bitstring, 0) + 1 | |
| return { | |
| 'success': True, | |
| 'statevector': state_data, | |
| 'measurements': { | |
| 'counts': counts, | |
| 'totalShots': shots | |
| }, | |
| 'circuit_data': self.circuit_data | |
| } | |
| except Exception as e: | |
| return { | |
| 'success': False, | |
| 'error': str(e) | |
| } | |
| def generate_circuit_svg(self) -> str: | |
| """Generate SVG visualization of the circuit""" | |
| try: | |
| from cirq.contrib.svg import circuit_to_svg | |
| return circuit_to_svg(self.circuit) | |
| except Exception as e: | |
| return f"<svg><text>Error generating circuit visualization: {str(e)}</text></svg>" | |
| def export_to_cirq_code(self) -> str: | |
| """Export the circuit to executable Cirq Python code""" | |
| code = ["import cirq", ""] | |
| code.append(f"# {self.circuit_data['name']}") | |
| code.append(f"# Created with GXS QuantumNexus Circuit Designer") | |
| code.append("") | |
| code.append(f"# Initialize {self.circuit_data['num_qubits']} qubits") | |
| code.append(f"qubits = [cirq.NamedQubit(f'q{{i}}') for i in range({self.circuit_data['num_qubits']})]") | |
| code.append("circuit = cirq.Circuit()") | |
| code.append("") | |
| # Generate gate operations | |
| for gate_data in self.circuit_data['gates']: | |
| gate_type = gate_data['type'] | |
| targets = gate_data['targets'] | |
| params = gate_data.get('params') | |
| if gate_type in self.SINGLE_QUBIT_GATES: | |
| # Handle parameterized gates | |
| if gate_type in ['Rx', 'Ry', 'Rz'] and params: | |
| angle = params.get('angle', 0) | |
| if gate_type == 'Rx': | |
| code.append(f"circuit.append(cirq.rx(rads={angle})(qubits[{targets[0]}]))") | |
| elif gate_type == 'Ry': | |
| code.append(f"circuit.append(cirq.ry(rads={angle})(qubits[{targets[0]}]))") | |
| elif gate_type == 'Rz': | |
| code.append(f"circuit.append(cirq.rz(rads={angle})(qubits[{targets[0]}]))") | |
| else: | |
| # Simple gates | |
| code.append(f"circuit.append(cirq.{gate_type}(qubits[{targets[0]}]))") | |
| elif gate_type in self.MULTI_QUBIT_GATES: | |
| target_strs = [f"qubits[{t}]" for t in targets] | |
| code.append(f"circuit.append(cirq.{gate_type}({', '.join(target_strs)}))") | |
| elif gate_type == 'MEASURE': | |
| code.append(f"circuit.append(cirq.measure(qubits[{targets[0]}], key='q{targets[0]}'))") | |
| # Add simulation code | |
| code.append("") | |
| code.append("# Simulate the circuit") | |
| code.append("simulator = cirq.Simulator()") | |
| code.append("result = simulator.simulate(circuit)") | |
| code.append("") | |
| code.append("# Print the final state vector") | |
| code.append("print('Final state vector:')") | |
| code.append("print(result.final_state_vector)") | |
| code.append("") | |
| code.append("# Run with measurements") | |
| code.append("sampler = cirq.Simulator()") | |
| code.append("result = sampler.run(circuit, repetitions=1000)") | |
| code.append("print('Measurement results:')") | |
| code.append("print(result)") | |
| return "\n".join(code) | |
| def save_circuit(self) -> str: | |
| """Save the circuit to a file and return the ID""" | |
| circuit_id = self.circuit_data['id'] | |
| filepath = os.path.join(CIRCUIT_DIR, f"{circuit_id}.json") | |
| with open(filepath, 'w') as f: | |
| json.dump(self.circuit_data, f, indent=2) | |
| return circuit_id | |
| def load_circuit(self, circuit_id: str) -> Dict[str, Any]: | |
| """Load a circuit from a file by ID""" | |
| filepath = os.path.join(CIRCUIT_DIR, f"{circuit_id}.json") | |
| if not os.path.exists(filepath): | |
| raise FileNotFoundError(f"Circuit with ID {circuit_id} not found") | |
| with open(filepath, 'r') as f: | |
| self.circuit_data = json.load(f) | |
| # Recreate qubits and circuit | |
| self.qubits = [cirq.NamedQubit(f'q{i}') for i in range(self.circuit_data['num_qubits'])] | |
| self.circuit = cirq.Circuit() | |
| # Rebuild the circuit | |
| for gate_data in self.circuit_data['gates']: | |
| gate_type = gate_data['type'] | |
| targets = gate_data['targets'] | |
| params = gate_data.get('params') | |
| self.add_gate(gate_type, targets, params) | |
| return self.circuit_data | |
| def list_saved_circuits(self) -> List[Dict[str, Any]]: | |
| """List all saved circuits""" | |
| circuits = [] | |
| for filename in os.listdir(CIRCUIT_DIR): | |
| if filename.endswith('.json'): | |
| filepath = os.path.join(CIRCUIT_DIR, filename) | |
| try: | |
| with open(filepath, 'r') as f: | |
| circuit_data = json.load(f) | |
| # Include minimal information | |
| circuits.append({ | |
| 'id': circuit_data['id'], | |
| 'name': circuit_data['name'], | |
| 'num_qubits': circuit_data['num_qubits'], | |
| 'depth': circuit_data['depth'] | |
| }) | |
| except: | |
| # Skip invalid files | |
| pass | |
| return sorted(circuits, key=lambda c: c['name']) | |
| # Flask routes for circuit designer API | |
| def register_circuit_designer_routes(app): | |
| """Register Flask routes for the circuit designer API""" | |
| designer = QuantumCircuitDesigner() | |
| def create_circuit(): | |
| """Create a new empty circuit""" | |
| data = request.json | |
| try: | |
| num_qubits = int(data.get('num_qubits', 3)) | |
| name = data.get('name', 'New Circuit') | |
| circuit_data = designer.create_empty_circuit(num_qubits, name) | |
| return jsonify({'success': True, 'circuit': circuit_data}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def add_gate(): | |
| """Add a gate to the circuit""" | |
| data = request.json | |
| try: | |
| gate_type = data.get('gate_type') | |
| targets = data.get('targets', []) | |
| params = data.get('params') | |
| if not gate_type: | |
| return jsonify({'success': False, 'error': 'Gate type is required'}), 400 | |
| circuit_data = designer.add_gate(gate_type, targets, params) | |
| return jsonify({'success': True, 'circuit': circuit_data}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def remove_gate(): | |
| """Remove a gate from the circuit""" | |
| data = request.json | |
| try: | |
| gate_index = int(data.get('gate_index', -1)) | |
| if gate_index < 0: | |
| return jsonify({'success': False, 'error': 'Valid gate index is required'}), 400 | |
| circuit_data = designer.remove_gate(gate_index) | |
| return jsonify({'success': True, 'circuit': circuit_data}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def simulate_circuit(): | |
| """Simulate the current circuit""" | |
| data = request.json | |
| try: | |
| shots = int(data.get('shots', 1024)) | |
| result = designer.simulate_circuit(shots) | |
| result['svg'] = designer.generate_circuit_svg() | |
| return jsonify(result) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def export_code(): | |
| """Export the circuit to Cirq code""" | |
| try: | |
| code = designer.export_to_cirq_code() | |
| return jsonify({'success': True, 'code': code}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def save_circuit(): | |
| """Save the current circuit""" | |
| data = request.json | |
| try: | |
| if data and 'name' in data: | |
| designer.circuit_data['name'] = data['name'] | |
| circuit_id = designer.save_circuit() | |
| return jsonify({'success': True, 'circuit_id': circuit_id}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def load_circuit(circuit_id): | |
| """Load a circuit by ID""" | |
| try: | |
| circuit_data = designer.load_circuit(circuit_id) | |
| return jsonify({'success': True, 'circuit': circuit_data}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 404 | |
| def list_circuits(): | |
| """List all saved circuits""" | |
| try: | |
| circuits = designer.list_saved_circuits() | |
| return jsonify({'success': True, 'circuits': circuits}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 500 | |
| def get_available_gates(): | |
| """Get list of available gates""" | |
| try: | |
| # Create serializable versions of the gate dictionaries | |
| single_qubit_gates = {} | |
| for gate_id, gate_info in designer.SINGLE_QUBIT_GATES.items(): | |
| single_qubit_gates[gate_id] = { | |
| 'name': gate_info['name'], | |
| 'type': 'single' | |
| } | |
| if 'params' in gate_info: | |
| single_qubit_gates[gate_id]['params'] = gate_info['params'] | |
| multi_qubit_gates = {} | |
| for gate_id, gate_info in designer.MULTI_QUBIT_GATES.items(): | |
| multi_qubit_gates[gate_id] = { | |
| 'name': gate_info['name'], | |
| 'type': 'multi', | |
| 'num_qubits': gate_info['num_qubits'] | |
| } | |
| gates = { | |
| 'single_qubit_gates': single_qubit_gates, | |
| 'multi_qubit_gates': multi_qubit_gates, | |
| 'special_gates': [ | |
| {'id': 'MEASURE', 'name': 'Measurement', 'type': 'special'} | |
| ] | |
| } | |
| return jsonify({'success': True, 'gates': gates}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 500 |