import json import math import cmath import numpy as np try: import plotly.graph_objects as go PLOTLY_AVAILABLE = True except ImportError: PLOTLY_AVAILABLE = False go = None # type: ignore from ..core.constants import GATE_LIBRARY, GATE_CATEGORIES def plot_bloch_sphere_plotly(statevector_data): """ Create an interactive 3D Bloch sphere using Plotly. Args: statevector_data: Can be: - dict with "real" and "imag" lists (from simulation) - dict with nested "statevector" key containing "real"/"imag" - list/tuple/array of complex numbers [alpha, beta] - None (returns default |0⟩ state) Returns: Plotly figure object compatible with gr.Plot(), or None if Plotly unavailable """ if not PLOTLY_AVAILABLE or go is None: return None # Default to |0⟩ state alpha, beta = 1.0 + 0j, 0.0 + 0j try: # Handle different statevector formats if statevector_data is None: pass # Use default |0⟩ elif isinstance(statevector_data, dict): # Check for nested "statevector" key (from simulation result) if "statevector" in statevector_data: sv_data = statevector_data["statevector"] if isinstance(sv_data, dict): real = sv_data.get("real", [1, 0]) imag = sv_data.get("imag", [0, 0]) else: real, imag = [1, 0], [0, 0] else: # Direct statevector dict {"real": [...], "imag": [...]} real = statevector_data.get("real", [1, 0]) imag = statevector_data.get("imag", [0, 0]) # Ensure we have at least 2 elements if len(real) >= 2 and len(imag) >= 2: alpha = complex(float(real[0]), float(imag[0])) beta = complex(float(real[1]), float(imag[1])) elif len(real) >= 2: alpha = complex(float(real[0]), 0) beta = complex(float(real[1]), 0) elif isinstance(statevector_data, (list, tuple, np.ndarray)): if len(statevector_data) >= 2: alpha = complex(statevector_data[0]) beta = complex(statevector_data[1]) except (TypeError, ValueError, IndexError) as e: # Fall back to |0⟩ state on any parsing error alpha, beta = 1.0 + 0j, 0.0 + 0j # Compute Bloch vector coordinates # For pure state |ψ⟩ = α|0⟩ + β|1⟩ # x = 2*Re(α*β̄), y = 2*Im(α*β̄), z = |α|² - |β|² x = float(2 * np.real(alpha * np.conj(beta))) y = float(2 * np.imag(alpha * np.conj(beta))) z = float(np.abs(alpha)**2 - np.abs(beta)**2) fig = go.Figure() # Create sphere wireframe (more visible than surface) # Equator circle theta_eq = np.linspace(0, 2*np.pi, 60) fig.add_trace(go.Scatter3d( x=np.cos(theta_eq), y=np.sin(theta_eq), z=np.zeros_like(theta_eq), mode='lines', line=dict(color='rgba(100,150,255,0.4)', width=2), hoverinfo='skip', showlegend=False )) # Meridian circles (XZ and YZ planes) for phi_offset in [0, np.pi/2]: phi_vals = np.linspace(0, 2*np.pi, 60) x_merid = np.cos(phi_offset) * np.sin(phi_vals) y_merid = np.sin(phi_offset) * np.sin(phi_vals) z_merid = np.cos(phi_vals) fig.add_trace(go.Scatter3d( x=x_merid, y=y_merid, z=z_merid, mode='lines', line=dict(color='rgba(100,150,255,0.3)', width=1), hoverinfo='skip', showlegend=False )) # Semi-transparent sphere surface u = np.linspace(0, 2 * np.pi, 25) v = np.linspace(0, np.pi, 25) x_sphere = np.outer(np.cos(u), np.sin(v)) y_sphere = np.outer(np.sin(u), np.sin(v)) z_sphere = np.outer(np.ones(np.size(u)), np.cos(v)) fig.add_trace(go.Surface( x=x_sphere, y=y_sphere, z=z_sphere, opacity=0.15, colorscale=[[0, 'rgb(70,130,180)'], [1, 'rgb(70,130,180)']], showscale=False, hoverinfo='skip' )) # Add coordinate axes with labels axis_length = 1.3 # X-axis (red) - |+⟩ to |-⟩ fig.add_trace(go.Scatter3d( x=[-axis_length, axis_length], y=[0, 0], z=[0, 0], mode='lines', line=dict(color='rgba(255,100,100,0.8)', width=3), hoverinfo='skip', showlegend=False )) fig.add_trace(go.Scatter3d( x=[axis_length*1.1], y=[0], z=[0], mode='text', text=['X |+⟩'], textfont=dict(size=12, color='#ff6b6b'), hoverinfo='skip', showlegend=False )) # Y-axis (green) - |R⟩ to |L⟩ fig.add_trace(go.Scatter3d( x=[0, 0], y=[-axis_length, axis_length], z=[0, 0], mode='lines', line=dict(color='rgba(100,255,100,0.8)', width=3), hoverinfo='skip', showlegend=False )) fig.add_trace(go.Scatter3d( x=[0], y=[axis_length*1.1], z=[0], mode='text', text=['Y |R⟩'], textfont=dict(size=12, color='#69db7c'), hoverinfo='skip', showlegend=False )) # Z-axis (blue) - |0⟩ to |1⟩ fig.add_trace(go.Scatter3d( x=[0, 0], y=[0, 0], z=[-axis_length, axis_length], mode='lines', line=dict(color='rgba(100,100,255,0.8)', width=3), hoverinfo='skip', showlegend=False )) fig.add_trace(go.Scatter3d( x=[0], y=[0], z=[axis_length*1.1], mode='text', text=['Z |0⟩'], textfont=dict(size=12, color='#748ffc'), hoverinfo='skip', showlegend=False )) fig.add_trace(go.Scatter3d( x=[0], y=[0], z=[-axis_length*1.1], mode='text', text=['|1⟩'], textfont=dict(size=12, color='#748ffc'), hoverinfo='skip', showlegend=False )) # Add state vector arrow (orange/gold) fig.add_trace(go.Scatter3d( x=[0, x], y=[0, y], z=[0, z], mode='lines', line=dict(color='#ffa500', width=6), hoverinfo='skip', showlegend=False )) # Add state vector point with hover info # Calculate theta and phi for display r = np.sqrt(x**2 + y**2 + z**2) theta_angle = np.arccos(z / r) if r > 0 else 0 phi_angle = np.arctan2(y, x) state_info = f"|ψ⟩
θ={theta_angle:.2f} rad
φ={phi_angle:.2f} rad
({x:.3f}, {y:.3f}, {z:.3f})" fig.add_trace(go.Scatter3d( x=[x], y=[y], z=[z], mode='markers', marker=dict(size=10, color='#ff4500', symbol='circle'), text=[state_info], hoverinfo='text', showlegend=False )) # Update layout for nice appearance fig.update_layout( scene=dict( xaxis=dict( range=[-1.5, 1.5], showbackground=False, showgrid=False, zeroline=False, showticklabels=False, title='' ), yaxis=dict( range=[-1.5, 1.5], showbackground=False, showgrid=False, zeroline=False, showticklabels=False, title='' ), zaxis=dict( range=[-1.5, 1.5], showbackground=False, showgrid=False, zeroline=False, showticklabels=False, title='' ), aspectmode='cube', camera=dict( eye=dict(x=1.8, y=1.8, z=1.2), up=dict(x=0, y=0, z=1) ), bgcolor='rgba(20,20,30,0.9)' ), title=dict( text='Bloch Sphere', x=0.5, xanchor='center', font=dict(size=16, color='#4fc3f7') ), showlegend=False, margin=dict(l=0, r=0, b=0, t=50), paper_bgcolor='rgba(20,20,30,0.95)', height=450 ) return fig def create_placeholder_plot(message: str) -> go.Figure: """Creates an empty Plotly figure with a text message. Used to display informative messages when visualization is not available, such as for multi-qubit circuits where Bloch sphere doesn't apply. Args: message: The message to display in the plot Returns: Plotly figure object with centered message """ if not PLOTLY_AVAILABLE or go is None: return None fig = go.Figure() fig.update_layout( xaxis=dict(visible=False, range=[0, 1]), yaxis=dict(visible=False, range=[0, 1]), annotations=[ dict( text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#78909c") ) ], paper_bgcolor='rgba(20,20,30,0.95)', plot_bgcolor='rgba(0,0,0,0)', height=450, margin=dict(l=20, r=20, b=20, t=50), title=dict( text='Bloch Sphere', x=0.5, xanchor='center', font=dict(size=16, color='#4fc3f7') ), ) return fig def render_bloch_sphere_svg(theta: float = 0, phi: float = 0, label: str = "|ψ⟩") -> str: """ Render an interactive Bloch sphere as SVG. theta: polar angle (0 to π) phi: azimuthal angle (0 to 2π) """ # Calculate Bloch vector coordinates x = math.sin(theta) * math.cos(phi) y = math.sin(theta) * math.sin(phi) z = math.cos(theta) # Project 3D to 2D (isometric-ish projection) cx, cy = 110, 120 # Center scale = 70 proj_x = cx + scale * (x * 0.866 - y * 0.5) proj_y = cy - scale * (z * 0.8 + (x * 0.5 + y * 0.866) * 0.3) return f''' Bloch Sphere |0⟩ |1⟩ +X -X {label} θ={theta:.2f}, φ={phi:.2f} ''' def render_qsphere_svg(probabilities: dict, num_qubits: int = 2) -> str: """ Render a Q-sphere visualization showing all basis states. Similar to IBM Quantum Composer's Q-sphere. """ if not probabilities: probabilities = {"0" * num_qubits: 1.0} dim = 2 ** num_qubits cx, cy = 150, 150 # Center radius = 100 svg = f''' Q-Sphere ({num_qubits} qubits) ''' # Place states on the sphere based on Hamming weight max_prob = max(probabilities.values()) if probabilities else 1.0 for i in range(dim): bitstring = format(i, f'0{num_qubits}b') prob = probabilities.get(bitstring, 0.0) if prob < 0.001: continue # Position based on Hamming weight (number of 1s) hamming = bitstring.count('1') layer_y = cy - radius + (2 * radius * hamming / num_qubits) # Spread states horizontally within each layer states_in_layer = math.comb(num_qubits, hamming) layer_radius = radius * math.sin(math.acos(1 - 2 * hamming / num_qubits)) if num_qubits > 0 else 0 # Find position within layer layer_states = [format(j, f'0{num_qubits}b') for j in range(dim) if format(j, f'0{num_qubits}b').count('1') == hamming] idx = layer_states.index(bitstring) angle = 2 * math.pi * idx / len(layer_states) if len(layer_states) > 0 else 0 state_x = cx + layer_radius * 0.7 * math.cos(angle) state_y = layer_y + layer_radius * 0.2 * math.sin(angle) # Size based on probability size = 5 + 15 * (prob / max_prob) # Color based on probability intensity = int(255 * (prob / max_prob)) color = f"rgb({intensity}, {200}, {255})" svg += f''' |{bitstring}⟩ {prob:.2f} ''' # North pole label svg += f'|{"0"*num_qubits}⟩' # South pole label svg += f'|{"1"*num_qubits}⟩' # Legend svg += ''' Size = Probability Vertical = Hamming weight (# of 1s) ''' svg += '' return svg def render_statevector_amplitudes(statevector_data: dict, num_qubits: int = 2) -> str: """ Render statevector amplitudes as a visual table with phase information. """ if not statevector_data: return "

No statevector data

" # Extract statevector sv = statevector_data.get("statevector", {}) if not sv: return "

No statevector available

" real_parts = sv.get("real", []) imag_parts = sv.get("imag", []) if not real_parts: return "

Empty statevector

" dim = len(real_parts) html = '''

📊 Statevector Amplitudes

''' for i in range(min(dim, 32)): # Limit to 32 states real = real_parts[i] imag = imag_parts[i] if i < len(imag_parts) else 0 amplitude = complex(real, imag) prob = abs(amplitude) ** 2 phase = cmath.phase(amplitude) if abs(amplitude) > 1e-10 else 0 if prob < 0.0001: continue bitstring = format(i, f'0{num_qubits}b') # Phase visualization (small arc) phase_deg = math.degrees(phase) phase_color = "#4fc3f7" if phase >= 0 else "#ff5722" html += f''' ''' html += '''
State Amplitude Prob Phase
|{bitstring}⟩ {real:.3f}{'+' if imag >= 0 else ''}{imag:.3f}i
{prob:.3f}
{phase_deg:.1f}°
''' return html def render_visual_circuit(gates_json: str, num_qubits: int = 4) -> str: """ Render an interactive visual circuit diagram like IBM Composer. Returns HTML/SVG for the circuit canvas. """ try: gates = json.loads(gates_json) if gates_json and gates_json != "[]" else [] except json.JSONDecodeError as e: # Return error SVG if gates JSON is invalid return f'
Invalid gates JSON: {e}
' # SVG dimensions wire_spacing = 50 gate_width = 40 gate_spacing = 60 left_margin = 80 top_margin = 30 canvas_width = max(600, left_margin + len(gates) * gate_spacing + 100) canvas_height = top_margin + num_qubits * wire_spacing + 50 # Start SVG svg = f''' ''' # Draw qubit labels and wires for i in range(num_qubits): y = top_margin + i * wire_spacing + 25 # Qubit label with ket notation svg += f''' |q{i}⟩ ''' # Horizontal wire svg += f''' ''' # Measurement icon at end svg += f''' ''' # Draw gates for idx, gate in enumerate(gates): name = gate.get("name", "?") qubits = gate.get("qubits", [0]) params = gate.get("params", []) gate_info = GATE_LIBRARY.get(name.lower(), {}) x = left_margin + idx * gate_spacing + 20 color = gate_info.get("color", "#607d8b") symbol = gate_info.get("symbol", name.upper()) if len(qubits) == 1: # Single qubit gate y = top_margin + qubits[0] * wire_spacing + 25 svg += f''' {symbol} ''' # Show parameter if exists if params: param_str = f"{params[0]:.2f}" if isinstance(params[0], float) else str(params[0]) svg += f''' θ={param_str} ''' elif len(qubits) >= 2: # Multi-qubit gate min_q = min(qubits) max_q = max(qubits) y1 = top_margin + min_q * wire_spacing + 25 y2 = top_margin + max_q * wire_spacing + 25 # Vertical connection line svg += f''' ''' # Control dots and targets for i, q in enumerate(qubits): y = top_margin + q * wire_spacing + 25 if name.lower() in ["cx", "ccx", "cy", "cz", "ch", "cswap"]: if i < len(qubits) - 1: # Control qubit - filled circle svg += f''' ''' else: # Target qubit if name.lower() in ["cx", "ccx"]: # X target (⊕) svg += f''' ''' else: # Other controlled gates svg += f''' {symbol[-1] if len(symbol) > 1 else symbol} ''' elif name.lower() == "swap": # SWAP gate (×) svg += f''' ''' # Add "drop zone" indicator if no gates if not gates: center_y = top_margin + (num_qubits - 1) * wire_spacing // 2 + 25 svg += f''' Click gates below to add ''' svg += '' return svg def render_gate_palette() -> str: """Render the IBM Composer-style gate palette.""" html = '''
''' # Group gates by category categories = {} for name, info in GATE_LIBRARY.items(): cat = info.get("category", "other") if cat not in categories: categories[cat] = [] categories[cat].append((name, info)) # Render each category for cat_key, cat_info in GATE_CATEGORIES.items(): if cat_key in categories: html += f'''
{cat_info["name"]}
''' for name, info in categories[cat_key]: html += f''' ''' html += '
' html += '
' return html def render_bloch_sphere_placeholder(statevector_json: str = "{}") -> str: """Render a Bloch sphere visualization placeholder.""" return '''
|0⟩
|1⟩
''' def render_probability_bars(results: dict) -> str: """Render probability distribution as HTML bars.""" counts = results.get("counts", {}) probs = results.get("probabilities", {}) if not probs: return "

No results available

" # Sort by bitstring sorted_keys = sorted(probs.keys()) html = '
' for key in sorted_keys: prob = probs[key] count = counts.get(key, 0) percent = prob * 100 html += f'''
|{key}⟩ {percent:.1f}% ({count})
''' html += '
' return html