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'''
'''
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'''
'
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 = '''| State | Amplitude | Prob | Phase |
|---|---|---|---|
| |{bitstring}⟩ | {real:.3f}{'+' if imag >= 0 else ''}{imag:.3f}i |
{prob:.3f}
|
{phase_deg:.1f}° |
No results available
" # Sort by bitstring sorted_keys = sorted(probs.keys()) html = '