File size: 12,950 Bytes
6ce350d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
"""
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