superquantx 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. superquantx/__init__.py +321 -0
  2. superquantx/algorithms/__init__.py +55 -0
  3. superquantx/algorithms/base_algorithm.py +413 -0
  4. superquantx/algorithms/hybrid_classifier.py +628 -0
  5. superquantx/algorithms/qaoa.py +406 -0
  6. superquantx/algorithms/quantum_agents.py +1006 -0
  7. superquantx/algorithms/quantum_kmeans.py +575 -0
  8. superquantx/algorithms/quantum_nn.py +544 -0
  9. superquantx/algorithms/quantum_pca.py +499 -0
  10. superquantx/algorithms/quantum_svm.py +346 -0
  11. superquantx/algorithms/vqe.py +553 -0
  12. superquantx/algorithms.py +863 -0
  13. superquantx/backends/__init__.py +265 -0
  14. superquantx/backends/base_backend.py +321 -0
  15. superquantx/backends/braket_backend.py +420 -0
  16. superquantx/backends/cirq_backend.py +466 -0
  17. superquantx/backends/ocean_backend.py +491 -0
  18. superquantx/backends/pennylane_backend.py +419 -0
  19. superquantx/backends/qiskit_backend.py +451 -0
  20. superquantx/backends/simulator_backend.py +455 -0
  21. superquantx/backends/tket_backend.py +519 -0
  22. superquantx/circuits.py +447 -0
  23. superquantx/cli/__init__.py +28 -0
  24. superquantx/cli/commands.py +528 -0
  25. superquantx/cli/main.py +254 -0
  26. superquantx/client.py +298 -0
  27. superquantx/config.py +326 -0
  28. superquantx/exceptions.py +287 -0
  29. superquantx/gates.py +588 -0
  30. superquantx/logging_config.py +347 -0
  31. superquantx/measurements.py +702 -0
  32. superquantx/ml.py +936 -0
  33. superquantx/noise.py +760 -0
  34. superquantx/utils/__init__.py +83 -0
  35. superquantx/utils/benchmarking.py +523 -0
  36. superquantx/utils/classical_utils.py +575 -0
  37. superquantx/utils/feature_mapping.py +467 -0
  38. superquantx/utils/optimization.py +410 -0
  39. superquantx/utils/quantum_utils.py +456 -0
  40. superquantx/utils/visualization.py +654 -0
  41. superquantx/version.py +33 -0
  42. superquantx-0.1.0.dist-info/METADATA +365 -0
  43. superquantx-0.1.0.dist-info/RECORD +46 -0
  44. superquantx-0.1.0.dist-info/WHEEL +4 -0
  45. superquantx-0.1.0.dist-info/entry_points.txt +2 -0
  46. superquantx-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,575 @@
1
+ """Quantum K-Means clustering implementation.
2
+
3
+ This module provides quantum algorithms for K-means clustering, including
4
+ quantum distance calculations and quantum amplitude estimation approaches.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, Optional, Union
9
+
10
+ import numpy as np
11
+ from sklearn.cluster import KMeans
12
+ from sklearn.metrics import adjusted_rand_score, silhouette_score
13
+ from sklearn.preprocessing import StandardScaler
14
+
15
+ from .base_algorithm import UnsupervisedQuantumAlgorithm
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class QuantumKMeans(UnsupervisedQuantumAlgorithm):
21
+ """Quantum K-Means clustering algorithm.
22
+
23
+ This implementation uses quantum algorithms to perform K-means clustering,
24
+ potentially offering speedup for high-dimensional data through quantum
25
+ distance calculations and amplitude estimation.
26
+
27
+ The algorithm can use different quantum approaches:
28
+ - Quantum Distance Calculation: Use quantum circuits to compute distances
29
+ - Quantum Amplitude Estimation: For probabilistic distance measurements
30
+ - Variational Quantum Clustering: Use VQC for cluster optimization
31
+ - Quantum Annealing: For global cluster optimization
32
+
33
+ Args:
34
+ backend: Quantum backend for circuit execution
35
+ n_clusters: Number of clusters (k)
36
+ method: Quantum method ('distance', 'amplitude', 'variational', 'annealing')
37
+ distance_metric: Distance metric ('euclidean', 'manhattan', 'quantum')
38
+ encoding: Data encoding method ('amplitude', 'angle', 'dense')
39
+ max_iterations: Maximum iterations for clustering
40
+ tolerance: Convergence tolerance
41
+ init_method: Centroid initialization ('random', 'k-means++', 'quantum')
42
+ shots: Number of measurement shots
43
+ classical_fallback: Use classical K-means if quantum fails
44
+ **kwargs: Additional parameters
45
+
46
+ Example:
47
+ >>> qkmeans = QuantumKMeans(backend='pennylane', n_clusters=3, method='distance')
48
+ >>> qkmeans.fit(X_train)
49
+ >>> labels = qkmeans.predict(X_test)
50
+ >>> centroids = qkmeans.cluster_centers_
51
+
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ backend: Union[str, Any],
57
+ n_clusters: int = 3,
58
+ method: str = 'distance',
59
+ distance_metric: str = 'euclidean',
60
+ encoding: str = 'amplitude',
61
+ max_iterations: int = 300,
62
+ tolerance: float = 1e-4,
63
+ init_method: str = 'k-means++',
64
+ shots: int = 1024,
65
+ classical_fallback: bool = True,
66
+ normalize_data: bool = True,
67
+ random_state: Optional[int] = None,
68
+ **kwargs
69
+ ) -> None:
70
+ super().__init__(backend=backend, shots=shots, **kwargs)
71
+
72
+ self.n_clusters = n_clusters
73
+ self.method = method
74
+ self.distance_metric = distance_metric
75
+ self.encoding = encoding
76
+ self.max_iterations = max_iterations
77
+ self.tolerance = tolerance
78
+ self.init_method = init_method
79
+ self.classical_fallback = classical_fallback
80
+ self.normalize_data = normalize_data
81
+ self.random_state = random_state
82
+
83
+ # Clustering results
84
+ self.cluster_centers_ = None
85
+ self.labels_ = None
86
+ self.inertia_ = None
87
+ self.n_iter_ = None
88
+
89
+ # Quantum-specific attributes
90
+ self.n_qubits = None
91
+ self.quantum_distances_ = None
92
+ self.convergence_history = []
93
+
94
+ # Classical components
95
+ self.scaler = StandardScaler() if normalize_data else None
96
+ self.classical_kmeans = KMeans(
97
+ n_clusters=n_clusters,
98
+ max_iter=max_iterations,
99
+ tol=tolerance,
100
+ random_state=random_state
101
+ )
102
+
103
+ # Set random seed
104
+ if random_state is not None:
105
+ np.random.seed(random_state)
106
+
107
+ logger.info(f"Initialized QuantumKMeans with method={method}, n_clusters={n_clusters}")
108
+
109
+ def _determine_qubits(self, n_features: int) -> int:
110
+ """Determine number of qubits needed for encoding."""
111
+ if self.encoding == 'amplitude':
112
+ return max(1, int(np.ceil(np.log2(n_features))))
113
+ elif self.encoding == 'angle':
114
+ return n_features
115
+ elif self.encoding == 'dense':
116
+ return n_features
117
+ else:
118
+ return max(1, int(np.ceil(np.log2(n_features))))
119
+
120
+ def _initialize_centroids(self, X: np.ndarray) -> np.ndarray:
121
+ """Initialize cluster centroids."""
122
+ n_samples, n_features = X.shape
123
+
124
+ if self.init_method == 'random':
125
+ # Random initialization
126
+ centroids = np.random.uniform(
127
+ X.min(axis=0), X.max(axis=0),
128
+ size=(self.n_clusters, n_features)
129
+ )
130
+ elif self.init_method == 'k-means++':
131
+ # K-means++ initialization
132
+ centroids = self._kmeans_plus_plus_init(X)
133
+ elif self.init_method == 'quantum':
134
+ # Quantum-inspired initialization
135
+ centroids = self._quantum_init(X)
136
+ else:
137
+ raise ValueError(f"Unknown initialization method: {self.init_method}")
138
+
139
+ return centroids
140
+
141
+ def _kmeans_plus_plus_init(self, X: np.ndarray) -> np.ndarray:
142
+ """K-means++ initialization algorithm."""
143
+ n_samples, n_features = X.shape
144
+ centroids = np.zeros((self.n_clusters, n_features))
145
+
146
+ # Choose first centroid randomly
147
+ centroids[0] = X[np.random.randint(n_samples)]
148
+
149
+ for i in range(1, self.n_clusters):
150
+ # Compute distances to nearest centroid
151
+ distances = np.array([
152
+ min([np.linalg.norm(x - c) ** 2 for c in centroids[:i]])
153
+ for x in X
154
+ ])
155
+
156
+ # Choose next centroid with probability proportional to distance^2
157
+ probabilities = distances / distances.sum()
158
+ cumulative_probs = probabilities.cumsum()
159
+ r = np.random.random()
160
+
161
+ for j, p in enumerate(cumulative_probs):
162
+ if r < p:
163
+ centroids[i] = X[j]
164
+ break
165
+
166
+ return centroids
167
+
168
+ def _quantum_init(self, X: np.ndarray) -> np.ndarray:
169
+ """Quantum-inspired centroid initialization."""
170
+ try:
171
+ if hasattr(self.backend, 'quantum_centroid_init'):
172
+ return self.backend.quantum_centroid_init(
173
+ X, self.n_clusters, encoding=self.encoding, shots=self.shots
174
+ )
175
+ else:
176
+ logger.warning("Quantum initialization not available, using k-means++")
177
+ return self._kmeans_plus_plus_init(X)
178
+ except Exception as e:
179
+ logger.error(f"Quantum initialization failed: {e}")
180
+ return self._kmeans_plus_plus_init(X)
181
+
182
+ def _encode_data_point(self, x: np.ndarray) -> Any:
183
+ """Encode data point into quantum state."""
184
+ try:
185
+ if hasattr(self.backend, 'encode_data_point'):
186
+ return self.backend.encode_data_point(
187
+ x, encoding=self.encoding, n_qubits=self.n_qubits
188
+ )
189
+ else:
190
+ return self._fallback_encoding(x)
191
+ except Exception as e:
192
+ logger.error(f"Data encoding failed: {e}")
193
+ return self._fallback_encoding(x)
194
+
195
+ def _fallback_encoding(self, x: np.ndarray) -> Any:
196
+ """Fallback data encoding."""
197
+ logger.warning("Using fallback data encoding")
198
+ return None
199
+
200
+ def _quantum_distance(self, x: np.ndarray, centroid: np.ndarray) -> float:
201
+ """Compute quantum distance between point and centroid."""
202
+ try:
203
+ if hasattr(self.backend, 'compute_quantum_distance'):
204
+ return self.backend.compute_quantum_distance(
205
+ x, centroid,
206
+ metric=self.distance_metric,
207
+ encoding=self.encoding,
208
+ n_qubits=self.n_qubits,
209
+ shots=self.shots
210
+ )
211
+ else:
212
+ return self._fallback_distance(x, centroid)
213
+ except Exception as e:
214
+ logger.error(f"Quantum distance computation failed: {e}")
215
+ return self._fallback_distance(x, centroid)
216
+
217
+ def _fallback_distance(self, x: np.ndarray, centroid: np.ndarray) -> float:
218
+ """Fallback classical distance computation."""
219
+ if self.distance_metric == 'euclidean':
220
+ return np.linalg.norm(x - centroid)
221
+ elif self.distance_metric == 'manhattan':
222
+ return np.sum(np.abs(x - centroid))
223
+ else:
224
+ return np.linalg.norm(x - centroid) # Default to euclidean
225
+
226
+ def _compute_distances_batch(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
227
+ """Compute distances between all points and all centroids."""
228
+ n_samples, n_features = X.shape
229
+ distances = np.zeros((n_samples, self.n_clusters))
230
+
231
+ if self.method == 'distance':
232
+ # Use quantum distance calculations
233
+ for i in range(n_samples):
234
+ for j in range(self.n_clusters):
235
+ distances[i, j] = self._quantum_distance(X[i], centroids[j])
236
+
237
+ elif self.method == 'amplitude':
238
+ # Use quantum amplitude estimation
239
+ distances = self._amplitude_estimation_distances(X, centroids)
240
+
241
+ elif self.method == 'variational':
242
+ # Use variational quantum circuits
243
+ distances = self._variational_distances(X, centroids)
244
+
245
+ else:
246
+ # Fallback to classical distances
247
+ for i in range(n_samples):
248
+ for j in range(self.n_clusters):
249
+ distances[i, j] = self._fallback_distance(X[i], centroids[j])
250
+
251
+ return distances
252
+
253
+ def _amplitude_estimation_distances(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
254
+ """Use quantum amplitude estimation for distance computation."""
255
+ try:
256
+ if hasattr(self.backend, 'amplitude_estimation_distances'):
257
+ return self.backend.amplitude_estimation_distances(
258
+ X, centroids,
259
+ encoding=self.encoding,
260
+ shots=self.shots
261
+ )
262
+ else:
263
+ logger.warning("Amplitude estimation not available, using classical")
264
+ return self._classical_distances(X, centroids)
265
+ except Exception as e:
266
+ logger.error(f"Amplitude estimation failed: {e}")
267
+ return self._classical_distances(X, centroids)
268
+
269
+ def _variational_distances(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
270
+ """Use variational quantum circuits for distance computation."""
271
+ try:
272
+ if hasattr(self.backend, 'variational_distances'):
273
+ return self.backend.variational_distances(
274
+ X, centroids,
275
+ encoding=self.encoding,
276
+ shots=self.shots
277
+ )
278
+ else:
279
+ logger.warning("Variational distances not available, using classical")
280
+ return self._classical_distances(X, centroids)
281
+ except Exception as e:
282
+ logger.error(f"Variational distance computation failed: {e}")
283
+ return self._classical_distances(X, centroids)
284
+
285
+ def _classical_distances(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
286
+ """Classical distance computation."""
287
+ n_samples = X.shape[0]
288
+ distances = np.zeros((n_samples, self.n_clusters))
289
+
290
+ for i in range(n_samples):
291
+ for j in range(self.n_clusters):
292
+ distances[i, j] = self._fallback_distance(X[i], centroids[j])
293
+
294
+ return distances
295
+
296
+ def _assign_clusters(self, distances: np.ndarray) -> np.ndarray:
297
+ """Assign points to clusters based on distances."""
298
+ return np.argmin(distances, axis=1)
299
+
300
+ def _update_centroids(self, X: np.ndarray, labels: np.ndarray) -> np.ndarray:
301
+ """Update cluster centroids."""
302
+ centroids = np.zeros((self.n_clusters, X.shape[1]))
303
+
304
+ for k in range(self.n_clusters):
305
+ cluster_points = X[labels == k]
306
+ if len(cluster_points) > 0:
307
+ centroids[k] = np.mean(cluster_points, axis=0)
308
+ else:
309
+ # Handle empty clusters - reinitialize randomly
310
+ centroids[k] = X[np.random.randint(len(X))]
311
+
312
+ return centroids
313
+
314
+ def _compute_inertia(self, X: np.ndarray, labels: np.ndarray, centroids: np.ndarray) -> float:
315
+ """Compute within-cluster sum of squares (inertia)."""
316
+ inertia = 0.0
317
+ for k in range(self.n_clusters):
318
+ cluster_points = X[labels == k]
319
+ if len(cluster_points) > 0:
320
+ distances = np.sum((cluster_points - centroids[k]) ** 2, axis=1)
321
+ inertia += np.sum(distances)
322
+ return inertia
323
+
324
+ def _check_convergence(self, old_centroids: np.ndarray, new_centroids: np.ndarray) -> bool:
325
+ """Check if centroids have converged."""
326
+ centroid_shift = np.max(np.linalg.norm(new_centroids - old_centroids, axis=1))
327
+ return centroid_shift < self.tolerance
328
+
329
+ def fit(self, X: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> 'QuantumKMeans':
330
+ """Fit quantum K-means to the data.
331
+
332
+ Args:
333
+ X: Training data
334
+ y: Ignored (unsupervised learning)
335
+ **kwargs: Additional fitting parameters
336
+
337
+ Returns:
338
+ Self for method chaining
339
+
340
+ """
341
+ logger.info(f"Fitting QuantumKMeans to data of shape {X.shape}")
342
+
343
+ # Validate and preprocess data
344
+ super().fit(X, y, **kwargs)
345
+
346
+ # Normalize data if specified
347
+ if self.normalize_data:
348
+ X = self.scaler.fit_transform(X)
349
+
350
+ # Determine quantum circuit requirements
351
+ self.n_qubits = self._determine_qubits(X.shape[1])
352
+
353
+ # Initialize centroids
354
+ centroids = self._initialize_centroids(X)
355
+
356
+ # Reset convergence history
357
+ self.convergence_history = []
358
+
359
+ # Main K-means iteration loop
360
+ for iteration in range(self.max_iterations):
361
+ # Store old centroids for convergence check
362
+ old_centroids = centroids.copy()
363
+
364
+ # Compute distances and assign clusters
365
+ distances = self._compute_distances_batch(X, centroids)
366
+ labels = self._assign_clusters(distances)
367
+
368
+ # Update centroids
369
+ centroids = self._update_centroids(X, labels)
370
+
371
+ # Compute inertia
372
+ inertia = self._compute_inertia(X, labels, centroids)
373
+ self.convergence_history.append(inertia)
374
+
375
+ # Check convergence
376
+ if self._check_convergence(old_centroids, centroids):
377
+ logger.info(f"Converged after {iteration + 1} iterations")
378
+ break
379
+
380
+ if iteration % 10 == 0:
381
+ logger.info(f"Iteration {iteration}: Inertia = {inertia:.6f}")
382
+
383
+ # Store final results
384
+ self.cluster_centers_ = centroids
385
+ self.labels_ = labels
386
+ self.inertia_ = inertia
387
+ self.n_iter_ = iteration + 1
388
+
389
+ # Fit classical K-means for comparison
390
+ if self.classical_fallback:
391
+ try:
392
+ self.classical_kmeans.fit(X)
393
+ except Exception as e:
394
+ logger.warning(f"Classical K-means fitting failed: {e}")
395
+
396
+ self.is_fitted = True
397
+
398
+ logger.info(f"Quantum K-means completed. Final inertia: {self.inertia_:.6f}")
399
+
400
+ return self
401
+
402
+ def predict(self, X: np.ndarray, **kwargs) -> np.ndarray:
403
+ """Predict cluster labels for new data.
404
+
405
+ Args:
406
+ X: Data to cluster
407
+ **kwargs: Additional parameters
408
+
409
+ Returns:
410
+ Cluster labels
411
+
412
+ """
413
+ if not self.is_fitted:
414
+ raise ValueError("QuantumKMeans must be fitted before prediction")
415
+
416
+ # Normalize data if specified
417
+ if self.normalize_data:
418
+ X = self.scaler.transform(X)
419
+
420
+ # Compute distances to centroids
421
+ distances = self._compute_distances_batch(X, self.cluster_centers_)
422
+
423
+ # Assign to nearest cluster
424
+ return self._assign_clusters(distances)
425
+
426
+ def fit_predict(self, X: np.ndarray, y: Optional[np.ndarray] = None) -> np.ndarray:
427
+ """Fit K-means and return cluster labels."""
428
+ return self.fit(X, y).labels_
429
+
430
+ def transform(self, X: np.ndarray) -> np.ndarray:
431
+ """Transform data to cluster-distance space."""
432
+ if not self.is_fitted:
433
+ raise ValueError("QuantumKMeans must be fitted before transform")
434
+
435
+ # Normalize data if specified
436
+ if self.normalize_data:
437
+ X = self.scaler.transform(X)
438
+
439
+ # Return distances to all centroids
440
+ return self._compute_distances_batch(X, self.cluster_centers_)
441
+
442
+ def get_quantum_advantage_metrics(self) -> Dict[str, Any]:
443
+ """Analyze potential quantum advantage."""
444
+ if not self.is_fitted:
445
+ raise ValueError("Must fit model first")
446
+
447
+ n_features = self.cluster_centers_.shape[1]
448
+ n_samples = self.n_samples_
449
+
450
+ metrics = {
451
+ 'data_dimension': n_features,
452
+ 'n_samples': n_samples,
453
+ 'n_clusters': self.n_clusters,
454
+ 'quantum_circuit_qubits': self.n_qubits,
455
+ 'encoding_efficiency': n_features / self.n_qubits if self.n_qubits > 0 else 1,
456
+ }
457
+
458
+ # Complexity estimates
459
+ classical_complexity = n_samples * self.n_clusters * n_features * self.n_iter_
460
+ quantum_complexity = n_samples * self.n_clusters * self.n_qubits * self.shots * self.n_iter_
461
+
462
+ metrics.update({
463
+ 'classical_complexity_estimate': classical_complexity,
464
+ 'quantum_complexity_estimate': quantum_complexity,
465
+ 'theoretical_speedup': classical_complexity / quantum_complexity if quantum_complexity > 0 else 1,
466
+ })
467
+
468
+ return metrics
469
+
470
+ def compare_with_classical(self, X: np.ndarray, y_true: Optional[np.ndarray] = None) -> Dict[str, Any]:
471
+ """Compare quantum K-means results with classical K-means."""
472
+ if not self.is_fitted or not hasattr(self.classical_kmeans, 'cluster_centers_'):
473
+ raise ValueError("Both quantum and classical K-means must be fitted")
474
+
475
+ # Get predictions from both methods
476
+ quantum_labels = self.predict(X)
477
+ classical_labels = self.classical_kmeans.predict(X if not self.normalize_data
478
+ else self.scaler.transform(X))
479
+
480
+ comparison = {
481
+ 'quantum_inertia': self.inertia_,
482
+ 'classical_inertia': self.classical_kmeans.inertia_,
483
+ 'inertia_ratio': self.inertia_ / self.classical_kmeans.inertia_ if self.classical_kmeans.inertia_ > 0 else float('inf'),
484
+ 'quantum_iterations': self.n_iter_,
485
+ 'classical_iterations': self.classical_kmeans.n_iter_,
486
+ }
487
+
488
+ # Compute silhouette scores
489
+ try:
490
+ if len(np.unique(quantum_labels)) > 1:
491
+ quantum_silhouette = silhouette_score(X, quantum_labels)
492
+ comparison['quantum_silhouette'] = quantum_silhouette
493
+
494
+ if len(np.unique(classical_labels)) > 1:
495
+ classical_silhouette = silhouette_score(X, classical_labels)
496
+ comparison['classical_silhouette'] = classical_silhouette
497
+
498
+ if 'quantum_silhouette' in comparison and 'classical_silhouette' in comparison:
499
+ comparison['silhouette_ratio'] = quantum_silhouette / classical_silhouette
500
+
501
+ except Exception as e:
502
+ logger.warning(f"Silhouette score computation failed: {e}")
503
+
504
+ # Compare with ground truth if available
505
+ if y_true is not None:
506
+ try:
507
+ quantum_ari = adjusted_rand_score(y_true, quantum_labels)
508
+ classical_ari = adjusted_rand_score(y_true, classical_labels)
509
+
510
+ comparison.update({
511
+ 'quantum_adjusted_rand_score': quantum_ari,
512
+ 'classical_adjusted_rand_score': classical_ari,
513
+ 'ari_ratio': quantum_ari / classical_ari if classical_ari != 0 else float('inf'),
514
+ })
515
+
516
+ except Exception as e:
517
+ logger.warning(f"Adjusted rand score computation failed: {e}")
518
+
519
+ # Compare centroid similarities
520
+ centroid_distances = []
521
+ for i in range(min(len(self.cluster_centers_), len(self.classical_kmeans.cluster_centers_))):
522
+ dist = np.linalg.norm(self.cluster_centers_[i] - self.classical_kmeans.cluster_centers_[i])
523
+ centroid_distances.append(dist)
524
+
525
+ if centroid_distances:
526
+ comparison.update({
527
+ 'centroid_distances': centroid_distances,
528
+ 'mean_centroid_distance': np.mean(centroid_distances),
529
+ 'max_centroid_distance': np.max(centroid_distances),
530
+ })
531
+
532
+ return comparison
533
+
534
+ def analyze_convergence(self) -> Dict[str, Any]:
535
+ """Analyze convergence properties."""
536
+ if not self.convergence_history:
537
+ return {'message': 'No convergence history available'}
538
+
539
+ inertias = np.array(self.convergence_history)
540
+
541
+ return {
542
+ 'total_iterations': len(inertias),
543
+ 'final_inertia': inertias[-1],
544
+ 'initial_inertia': inertias[0],
545
+ 'inertia_reduction': inertias[0] - inertias[-1] if len(inertias) > 0 else 0,
546
+ 'convergence_rate': np.mean(np.diff(inertias)) if len(inertias) > 1 else 0,
547
+ 'converged': len(inertias) < self.max_iterations,
548
+ 'inertia_variance': np.var(inertias[-10:]) if len(inertias) >= 10 else np.var(inertias),
549
+ }
550
+
551
+ def get_params(self, deep: bool = True) -> Dict[str, Any]:
552
+ """Get quantum K-means parameters."""
553
+ params = super().get_params(deep)
554
+ params.update({
555
+ 'n_clusters': self.n_clusters,
556
+ 'method': self.method,
557
+ 'distance_metric': self.distance_metric,
558
+ 'encoding': self.encoding,
559
+ 'max_iterations': self.max_iterations,
560
+ 'tolerance': self.tolerance,
561
+ 'init_method': self.init_method,
562
+ 'classical_fallback': self.classical_fallback,
563
+ 'normalize_data': self.normalize_data,
564
+ 'random_state': self.random_state,
565
+ })
566
+ return params
567
+
568
+ def set_params(self, **params) -> 'QuantumKMeans':
569
+ """Set quantum K-means parameters."""
570
+ if self.is_fitted and any(key in params for key in
571
+ ['n_clusters', 'method', 'distance_metric', 'encoding']):
572
+ logger.warning("Changing core parameters requires refitting the model")
573
+ self.is_fitted = False
574
+
575
+ return super().set_params(**params)