QuantumArchitect-MCP / src /plugins /scoring /expressibility_score.py
Deminiko
Initial commit: QuantumArchitect-MCP quantum circuit MCP server with Gradio UI
6ce350d
"""
Expressibility Score - Evaluates how well variational circuits can cover the Hilbert space.
Research-level metric for Quantum Machine Learning (QML) applications.
"""
from typing import Any
import numpy as np
import math
def score_expressibility(
circuit_data: dict[str, Any],
num_samples: int = 1000
) -> dict[str, Any]:
"""
Score the expressibility of a variational quantum circuit.
Expressibility measures how uniformly a parameterized circuit can sample
from the space of quantum states. Higher expressibility = better for QML.
Based on: "Expressibility and Entangling Capability of Parameterized
Quantum Circuits for Hybrid Quantum-Classical Algorithms"
Args:
circuit_data: Circuit dictionary (should be parameterized)
num_samples: Number of random parameter samples for estimation
Returns:
Expressibility analysis
"""
gates = circuit_data.get("gates", [])
num_qubits = circuit_data.get("num_qubits", 0)
parameters = circuit_data.get("parameters", [])
# Analyze circuit structure
structure_analysis = _analyze_ansatz_structure(gates, num_qubits)
# Calculate theoretical expressibility bounds
num_params = len(parameters) if parameters else structure_analysis["estimated_parameters"]
# Expressibility capacity
# Maximum expressibility requires 2^(2n) - 1 parameters for n qubits
# (full SU(2^n) coverage)
max_params_needed = 4 ** num_qubits - 1
param_sufficiency = min(1.0, num_params / max_params_needed) if max_params_needed > 0 else 0
# Entangling capability analysis
entangling_score = _calculate_entangling_capability(gates, num_qubits)
# Estimate expressibility score (0-1)
# Based on: parameter count, entangling gates, layer structure
expressibility_score = _estimate_expressibility(
num_params,
num_qubits,
structure_analysis,
entangling_score
)
# Meyer-Wallach entanglement measure estimate
mw_measure = _estimate_meyer_wallach(structure_analysis, num_qubits)
# Classification
if expressibility_score > 0.8:
classification = "Highly Expressive"
recommendation = "Suitable for complex learning tasks"
elif expressibility_score > 0.5:
classification = "Moderately Expressive"
recommendation = "Good for moderate complexity problems"
elif expressibility_score > 0.2:
classification = "Low Expressibility"
recommendation = "May struggle with complex patterns"
else:
classification = "Very Low Expressibility"
recommendation = "Consider adding more parameters or entangling gates"
return {
"expressibility_score": round(expressibility_score, 4),
"classification": classification,
"recommendation": recommendation,
"analysis": {
"num_parameters": num_params,
"max_parameters_for_full_coverage": max_params_needed,
"parameter_sufficiency": round(param_sufficiency, 4),
"entangling_capability": round(entangling_score, 4),
"estimated_meyer_wallach": round(mw_measure, 4),
},
"structure": {
"num_layers": structure_analysis["num_layers"],
"entangling_gates_per_layer": structure_analysis["entangling_per_layer"],
"rotation_gates_per_layer": structure_analysis["rotations_per_layer"],
"entanglement_pattern": structure_analysis["entanglement_pattern"],
},
"qml_suitability": {
"classification_tasks": "Good" if expressibility_score > 0.3 else "Limited",
"regression_tasks": "Good" if expressibility_score > 0.4 else "Limited",
"generative_tasks": "Good" if expressibility_score > 0.6 else "Limited",
},
}
def _analyze_ansatz_structure(
gates: list[dict[str, Any]],
num_qubits: int
) -> dict[str, Any]:
"""Analyze the structure of a variational ansatz."""
rotation_gates = {"rx", "ry", "rz", "u", "u1", "u2", "u3", "p"}
entangling_gates = {"cx", "cnot", "cz", "cy", "swap", "crx", "cry", "crz", "cp"}
rotation_count = 0
entangling_count = 0
barriers = 0
estimated_params = 0
# Track layers (separated by barriers or pattern)
current_layer_rotations = 0
current_layer_entangling = 0
layers: list[dict[str, int]] = []
# Entanglement pattern detection
entangling_pairs: list[tuple[int, int]] = []
for gate in gates:
name = gate.get("name", "").lower()
qubits = gate.get("qubits", [])
if name == "barrier":
if current_layer_rotations > 0 or current_layer_entangling > 0:
layers.append({
"rotations": current_layer_rotations,
"entangling": current_layer_entangling
})
current_layer_rotations = 0
current_layer_entangling = 0
barriers += 1
continue
if name in rotation_gates:
rotation_count += 1
current_layer_rotations += 1
# Estimate parameters
if name in ("rx", "ry", "rz", "p", "u1"):
estimated_params += 1
elif name in ("u2",):
estimated_params += 2
elif name in ("u", "u3"):
estimated_params += 3
elif name in entangling_gates:
entangling_count += 1
current_layer_entangling += 1
if len(qubits) == 2:
entangling_pairs.append((qubits[0], qubits[1]))
# Add final layer if exists
if current_layer_rotations > 0 or current_layer_entangling > 0:
layers.append({
"rotations": current_layer_rotations,
"entangling": current_layer_entangling
})
# Detect entanglement pattern
pattern = _detect_entanglement_pattern(entangling_pairs, num_qubits)
num_layers = len(layers) if layers else 1
return {
"rotation_count": rotation_count,
"entangling_count": entangling_count,
"estimated_parameters": estimated_params,
"num_layers": num_layers,
"rotations_per_layer": rotation_count / num_layers if num_layers > 0 else 0,
"entangling_per_layer": entangling_count / num_layers if num_layers > 0 else 0,
"entanglement_pattern": pattern,
"layers_detail": layers,
}
def _detect_entanglement_pattern(
pairs: list[tuple[int, int]],
num_qubits: int
) -> str:
"""Detect the entanglement pattern."""
if not pairs:
return "none"
# Check for linear pattern
linear_pairs = [(i, i+1) for i in range(num_qubits - 1)]
linear_pairs_rev = [(i+1, i) for i in range(num_qubits - 1)]
if all(p in pairs or (p[1], p[0]) in pairs for p in linear_pairs):
# Check if circular
if (0, num_qubits - 1) in pairs or (num_qubits - 1, 0) in pairs:
return "circular"
return "linear"
# Check for all-to-all
all_pairs = [(i, j) for i in range(num_qubits) for j in range(i+1, num_qubits)]
if all(p in pairs or (p[1], p[0]) in pairs for p in all_pairs):
return "full"
# Check for alternating
even_pairs = [(i, i+1) for i in range(0, num_qubits - 1, 2)]
odd_pairs = [(i, i+1) for i in range(1, num_qubits - 1, 2)]
if all(p in pairs or (p[1], p[0]) in pairs for p in even_pairs):
if all(p in pairs or (p[1], p[0]) in pairs for p in odd_pairs):
return "alternating"
return "custom"
def _calculate_entangling_capability(
gates: list[dict[str, Any]],
num_qubits: int
) -> float:
"""Calculate entangling capability score."""
entangling_gates = {"cx", "cnot", "cz", "cy", "swap", "crx", "cry", "crz", "cp"}
entangling_count = 0
qubit_pairs_entangled: set[tuple[int, int]] = set()
for gate in gates:
name = gate.get("name", "").lower()
qubits = gate.get("qubits", [])
if name in entangling_gates and len(qubits) >= 2:
entangling_count += 1
pair = (min(qubits[0], qubits[1]), max(qubits[0], qubits[1]))
qubit_pairs_entangled.add(pair)
# Maximum possible pairs
max_pairs = num_qubits * (num_qubits - 1) // 2
# Coverage score
coverage = len(qubit_pairs_entangled) / max_pairs if max_pairs > 0 else 0
# Depth score (more entangling layers = more capability)
depth_factor = min(1.0, entangling_count / (num_qubits * 2))
return (coverage + depth_factor) / 2
def _estimate_expressibility(
num_params: int,
num_qubits: int,
structure: dict[str, Any],
entangling_score: float
) -> float:
"""Estimate expressibility score."""
# Base score from parameter count
dim = 4 ** num_qubits
param_score = min(1.0, num_params / (dim / 4))
# Structure bonus
structure_score = 0.0
if structure["num_layers"] >= 2:
structure_score += 0.1
if structure["entangling_per_layer"] >= num_qubits - 1:
structure_score += 0.2
if structure["entanglement_pattern"] in ("full", "circular"):
structure_score += 0.1
# Combine scores
score = 0.4 * param_score + 0.3 * entangling_score + 0.3 * min(1.0, structure_score)
return min(1.0, score)
def _estimate_meyer_wallach(structure: dict[str, Any], num_qubits: int) -> float:
"""Estimate Meyer-Wallach entanglement measure."""
# Simplified estimation based on entanglement structure
entangling_count = structure["entangling_count"]
if entangling_count == 0:
return 0.0
# Rough estimate: MW measure approaches 0.5 for maximally entangling circuits
max_entangling = num_qubits * (num_qubits - 1)
coverage = min(1.0, entangling_count / max_entangling)
return 0.5 * coverage
def analyze_ansatz_trainability(circuit_data: dict[str, Any]) -> dict[str, Any]:
"""
Analyze potential trainability issues like barren plateaus.
Barren plateaus occur when gradients vanish exponentially with
circuit depth/width, making training difficult.
"""
gates = circuit_data.get("gates", [])
num_qubits = circuit_data.get("num_qubits", 0)
structure = _analyze_ansatz_structure(gates, num_qubits)
# Risk factors for barren plateaus
risk_factors = []
risk_score = 0.0
# Factor 1: Circuit depth
depth_ratio = structure["num_layers"] / num_qubits if num_qubits > 0 else 0
if depth_ratio > 10:
risk_factors.append("Very deep circuit (high barren plateau risk)")
risk_score += 0.3
elif depth_ratio > 5:
risk_factors.append("Deep circuit (moderate barren plateau risk)")
risk_score += 0.15
# Factor 2: Global entanglement
if structure["entanglement_pattern"] == "full":
risk_factors.append("Full entanglement pattern increases gradient variance")
risk_score += 0.2
# Factor 3: Many random parameters
if structure["estimated_parameters"] > 4 ** num_qubits // 4:
risk_factors.append("High parameter count relative to Hilbert space")
risk_score += 0.15
# Factor 4: Large qubit count
if num_qubits > 10:
risk_factors.append(f"Large qubit count ({num_qubits}) increases plateau risk")
risk_score += 0.2
# Recommendations
recommendations = []
if risk_score > 0.3:
recommendations.append("Consider parameter initialization strategies (e.g., identity initialization)")
recommendations.append("Use local cost functions when possible")
recommendations.append("Consider layer-wise training")
if structure["entanglement_pattern"] == "full":
recommendations.append("Consider using linear or alternating entanglement instead")
if not risk_factors:
risk_factors.append("No major barren plateau risk factors detected")
return {
"barren_plateau_risk": round(min(1.0, risk_score), 2),
"risk_level": "High" if risk_score > 0.4 else "Moderate" if risk_score > 0.2 else "Low",
"risk_factors": risk_factors,
"recommendations": recommendations,
"trainability_friendly_features": _identify_good_features(structure),
}
def _identify_good_features(structure: dict[str, Any]) -> list[str]:
"""Identify features that help trainability."""
good_features = []
if structure["entanglement_pattern"] == "linear":
good_features.append("Linear entanglement pattern (good for avoiding barren plateaus)")
if structure["num_layers"] <= 3:
good_features.append("Shallow circuit depth")
if structure["rotations_per_layer"] > 0:
good_features.append("Sufficient rotation gates for expressivity")
return good_features