Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Spike: `QuantumSVM(feature_map="auto")` Qmes-style encoding selection with
optional Qmes extra and a classical heuristic fallback
(`docs/design/qmes-feature-map-auto.md`).

### Planned
- Quantum reinforcement learning algorithms
- Quantum natural language processing
Expand Down
107 changes: 107 additions & 0 deletions docs/design/qmes-feature-map-auto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Design note: QuantumSVM `feature_map="auto"` (Qmes-style spike)

Status: spike on branch `spike/qmes-feature-map-auto`.
Inspired by [Qmes](https://github.com/tungduy1704/Qmes) ([arXiv:2609.04652](https://arxiv.org/abs/2609.04652)).

## Why

QuantumSVM performance depends strongly on the encoding circuit. Exhaustive
kernel evaluation of every candidate is expensive. Qmes shows that classical
dataset complexity features can rank encoding circuits without quantum
evaluation at inference time. This spike adds a practical `feature_map="auto"`
path that recommends a name from SuperQuantX's existing feature-map registry.

## What we adopted

1. **Registry** (`utils/feature_map_registry.py`)
Canonical names: `ZZFeatureMap`, `PauliFeatureMap`, `AmplitudeMap`,
`AngleEncoding`, `ZFeatureMap`. Manual string aliases still work.

2. **Qmes mapping** (`QMES_TO_SUPERQUANTX`)

| Qmes pool | SuperQuantX registry | Rationale |
|---|---|---|
| unit | AmplitudeMap | Amplitude / square-root style embedding |
| SRx | AngleEncoding | Separable single-qubit rotations |
| RY | AngleEncoding | Separable RY angle encoding |
| HERx | PauliFeatureMap | Hardware-efficient rotations + CX |
| RY_CX | PauliFeatureMap | Angle + linear CX |
| ZFM | ZFeatureMap | Separable Z rotations |
| HD | ZZFeatureMap | High-dim / entangling analogue |

3. **Selector** (`utils/feature_map_auto.py`)
- `mode="auto"`: try Qmes if installed, else heuristic.
- `mode="qmes"`: require optional `[qmes]` extra.
- `mode="heuristic"`: local meta-features only (no problexity, no quantum).
- Heuristic uses sample/feature ratio, mean |correlation|, class imbalance,
and a cheap logistic-regression separability proxy.

4. **QuantumSVM**
- `feature_map="auto"` or `None` resolves at `fit()` time.
- Stores `selected_feature_map_` and `feature_map_recommendation_`.
- Existing manual strings/objects are unchanged.

5. **Backends**
Simulator / PennyLane / Qiskit / Cirq `create_feature_map` recognize
`AngleEncoding` and `ZFeatureMap` (AmplitudeMap already present).

## What we deferred

- Full Qmes circuit pool implementations (unit, SRx, HERx, RY_CX, HD gate
schedules, Qsun backend, fixed 4-qubit PCA pipeline).
- Shipping or re-training a SuperQuantX-native pairwise OvO recommender on
our own meta-dataset and backends.
- Problexity's full 22-d / 12-d complexity vectors in the default heuristic.
- Noise-aware or hardware-aware recommendation.
- Regression-task auto selection wired into a QuantumSVR (QSVM remains
classification-focused).
- Changing the default constructor value away from `ZZFeatureMap` (auto is
opt-in).

## Circuit-pool gaps

Qmes evaluates seven Qsun encodings with a shared 4-qubit PCA + MinMax
preprocess. SuperQuantX backends implement a smaller, differently shaped set.
Closest-name mapping is approximate:

- Exact Qmes unit / HD gate lists are not reproduced.
- `AmplitudeMap` remains a partial/placeholder on several backends.
- Entanglement patterns (linear CX vs ISWAP brickwork) differ.
- Recommendation quality of the heuristic is unvalidated against Qmes regret.

## How to try

```python
from sklearn.datasets import make_classification
from superquantx.algorithms import QuantumSVM

X, y = make_classification(n_samples=40, n_features=4, n_informative=3, random_state=0)
qsvm = QuantumSVM(backend="simulator", feature_map="auto", shots=100)
qsvm.fit(X, y)
print(qsvm.selected_feature_map_)
print(qsvm.feature_map_recommendation_)
```

Optional Qmes path:

```bash
pip install "superquantx[qmes]"
# or: pip install "git+https://github.com/tungduy1704/Qmes.git"
```

```python
qsvm = QuantumSVM(
backend="simulator",
feature_map="auto",
auto_feature_map_mode="qmes",
)
```

## Follow-ups

1. Expand the registry with first-class Qmes-like encodings (or adapters).
2. Build a SuperQuantX meta-dataset on simulator kernels and train a pairwise
recommender (or fine-tune Qmes with registered SQX circuits).
3. Expose `recommend_feature_map` in the CLI / docs tutorials.
4. Add regression auto-selection once a Quantum kernel regressor lands.
5. Benchmark heuristic vs Qmes vs fixed `ZZFeatureMap` on shared datasets.
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ ocean = [
"dimod>=0.12.22",
]

# Optional Qmes-backed feature_map="auto" recommender
# (https://github.com/tungduy1704/Qmes , arXiv:2609.04652).
# Install from git until a stable PyPI release is available.
qmes = [
"Qmes @ git+https://github.com/tungduy1704/Qmes.git",
"problexity>=0.5.0",
"pandas>=1.3",
]

# Development dependencies - latest versions
dev = [
"pytest>=8.0.0",
Expand Down Expand Up @@ -265,6 +274,8 @@ module = [
"optuna.*",
"hyperopt.*",
"cvxpy.*",
"Qmes.*",
"problexity.*",
]
ignore_missing_imports = true

Expand Down
64 changes: 57 additions & 7 deletions src/superquantx/algorithms/quantum_svm.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,40 @@ class QuantumSVM(SupervisedQuantumAlgorithm):

Args:
backend: Quantum backend for circuit execution
feature_map: Type of quantum feature map ('ZZFeatureMap', 'PauliFeatureMap', etc.)
feature_map: Feature map name ('ZZFeatureMap', 'PauliFeatureMap',
'AmplitudeMap', 'AngleEncoding', 'ZFeatureMap'), a custom object,
or ``"auto"`` / ``None`` to select via the Qmes-inspired recommender.
feature_map_reps: Number of repetitions in the feature map
C: Regularization parameter for SVM
gamma: Kernel coefficient (for RBF-like quantum kernels)
quantum_kernel: Custom quantum kernel function
shots: Number of measurement shots
auto_feature_map_mode: When feature_map is auto/None, selection mode
(``auto``, ``qmes``, or ``heuristic``).
**kwargs: Additional parameters

Example:
>>> qsvm = QuantumSVM(backend='pennylane', feature_map='ZZFeatureMap')
>>> qsvm.fit(X_train, y_train)
>>> predictions = qsvm.predict(X_test)
>>> accuracy = qsvm.score(X_test, y_test)
>>> # Spike: automatic encoding selection (Qmes if installed, else heuristic)
>>> qsvm_auto = QuantumSVM(backend='simulator', feature_map='auto')
>>> qsvm_auto.fit(X_train, y_train)

"""

def __init__(
self,
backend: str | Any,
feature_map: str = 'ZZFeatureMap',
feature_map: str | Any | None = 'ZZFeatureMap',
feature_map_reps: int = 2,
C: float = 1.0,
gamma: float | None = None,
quantum_kernel: Callable | None = None,
shots: int = 1024,
normalize_data: bool = True,
auto_feature_map_mode: str = 'auto',
**kwargs
) -> None:
super().__init__(backend=backend, shots=shots, **kwargs)
Expand All @@ -68,6 +76,7 @@ def __init__(
self.gamma = gamma
self.quantum_kernel = quantum_kernel
self.normalize_data = normalize_data
self.auto_feature_map_mode = auto_feature_map_mode

# Classical components
self.svm = None
Expand All @@ -76,19 +85,56 @@ def __init__(
# Quantum components
self.kernel_matrix_ = None
self.feature_map_circuit_ = None
self.selected_feature_map_: str | Any | None = None
self.feature_map_recommendation_: dict[str, Any] | None = None

# Training data storage (needed for kernel computation)
self.X_train_ = None

logger.info(f"Initialized QuantumSVM with feature_map={feature_map}, reps={feature_map_reps}")
logger.info(
f"Initialized QuantumSVM with feature_map={feature_map}, "
f"reps={feature_map_reps}"
)

def _create_feature_map(self, n_features: int) -> Any:
def _resolve_feature_map_name(
self, X: np.ndarray, y: np.ndarray | None = None
) -> str | Any:
"""Resolve feature_map='auto'/None or normalize a manual string."""
from ..utils.feature_map_auto import resolve_feature_map

recommendation = resolve_feature_map(
self.feature_map,
X=X,
y=y,
auto_mode=self.auto_feature_map_mode, # type: ignore[arg-type]
task='classification',
)
self.feature_map_recommendation_ = recommendation.as_dict()
self.selected_feature_map_ = recommendation.feature_map
if recommendation.source in ('auto', 'qmes', 'heuristic', 'default'):
logger.info(
"Auto-selected feature_map=%s via %s",
recommendation.feature_map,
recommendation.source,
)
return recommendation.feature_map

def _create_feature_map(self, n_features: int, feature_map: str | Any | None = None) -> Any:
"""Create quantum feature map circuit."""
fm = feature_map if feature_map is not None else self.selected_feature_map_
if fm is None:
fm = self.feature_map if self.feature_map not in (None, 'auto') else 'ZZFeatureMap'

# Custom non-string feature map objects are returned as-is for backends
# that already understand them.
if not isinstance(fm, str):
return fm

try:
if hasattr(self.backend, 'create_feature_map'):
return self.backend.create_feature_map(
n_features=n_features,
feature_map=self.feature_map,
feature_map=fm,
reps=self.feature_map_reps
)
else:
Expand Down Expand Up @@ -166,8 +212,9 @@ def fit(self, X: np.ndarray, y: np.ndarray, **kwargs) -> 'QuantumSVM':

self.X_train_ = X.copy()

# Create quantum feature map
self.feature_map_circuit_ = self._create_feature_map(X.shape[1])
# Resolve auto/manual feature map, then build the circuit
resolved = self._resolve_feature_map_name(X, y)
self.feature_map_circuit_ = self._create_feature_map(X.shape[1], resolved)

# Compute quantum kernel matrix
logger.info("Computing quantum kernel matrix...")
Expand All @@ -190,6 +237,8 @@ def fit(self, X: np.ndarray, y: np.ndarray, **kwargs) -> 'QuantumSVM':
'train_accuracy': train_accuracy,
'n_support_vectors': self.svm.n_support_,
'kernel_matrix_shape': self.kernel_matrix_.shape,
'selected_feature_map': self.selected_feature_map_,
'feature_map_recommendation': self.feature_map_recommendation_,
})

logger.info(f"Training completed. Accuracy: {train_accuracy:.3f}, "
Expand Down Expand Up @@ -334,6 +383,7 @@ def get_params(self, deep: bool = True) -> dict[str, Any]:
'C': self.C,
'gamma': self.gamma,
'normalize_data': self.normalize_data,
'auto_feature_map_mode': self.auto_feature_map_mode,
})
return params

Expand Down
13 changes: 13 additions & 0 deletions src/superquantx/backends/cirq_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,25 @@ def create_feature_map(self, n_features: int, feature_map: str, reps: int = 1) -
circuit_info = self._create_pauli_feature_map(n_features, reps)
elif feature_map == 'AmplitudeMap':
circuit_info = self._create_amplitude_map(n_features)
elif feature_map == 'ZFeatureMap':
circuit_info = self._create_z_feature_map(n_features, reps)
elif feature_map == 'AngleEncoding':
circuit_info = self._create_angle_encoding_map(n_features)
else:
logger.warning(f"Unknown feature map '{feature_map}', using angle encoding")
circuit_info = self._create_angle_encoding_map(n_features)

return circuit_info

def _create_z_feature_map(self, n_features: int, reps: int) -> tuple[Any, list]:
"""Create separable Z-rotation feature map."""
qubits = [cirq.LineQubit(i) for i in range(n_features)]
circuit = cirq.Circuit()
for r in range(reps):
for i, q in enumerate(qubits):
circuit.append(cirq.rz(cirq.Symbol(f'x_{i}_z_{r}'))(q))
return circuit, qubits

def _create_zz_feature_map(self, n_features: int, reps: int) -> tuple[Any, list]:
"""Create ZZ feature map circuit."""
qubits = [cirq.LineQubit(i) for i in range(n_features)]
Expand Down
14 changes: 14 additions & 0 deletions src/superquantx/backends/pennylane_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,24 @@ def create_feature_map(self, n_features: int, feature_map: str, reps: int = 1) -
return self._create_pauli_feature_map(n_features, reps)
elif feature_map == 'AmplitudeMap':
return self._create_amplitude_map(n_features)
elif feature_map == 'ZFeatureMap':
return self._create_z_feature_map(n_features, reps)
elif feature_map == 'AngleEncoding':
return self._create_angle_encoding_map(n_features)
else:
logger.warning(f"Unknown feature map '{feature_map}', using angle encoding")
return self._create_angle_encoding_map(n_features)

def _create_z_feature_map(self, n_features: int, reps: int) -> Callable:
"""Create separable Z-rotation feature map."""
def z_feature_map(x):
for r in range(reps):
for i in range(min(n_features, self.wires)):
qml.RZ(2.0 * x[i], wires=i)
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_features, self.wires))]

return z_feature_map

def _create_zz_feature_map(self, n_features: int, reps: int) -> Callable:
"""Create ZZ feature map circuit."""
def zz_feature_map(x):
Expand Down
12 changes: 12 additions & 0 deletions src/superquantx/backends/qiskit_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,22 @@ def create_feature_map(self, n_features: int, feature_map: str, reps: int = 1) -
return self._create_pauli_feature_map(n_features, reps)
elif feature_map == 'AmplitudeMap':
return self._create_amplitude_map(n_features)
elif feature_map == 'ZFeatureMap':
return self._create_z_feature_map(n_features, reps)
elif feature_map == 'AngleEncoding':
return self._create_angle_encoding_map(n_features)
else:
logger.warning(f"Unknown feature map '{feature_map}', using angle encoding")
return self._create_angle_encoding_map(n_features)

def _create_z_feature_map(self, n_features: int, reps: int) -> Any:
"""Create separable Z-rotation feature map."""
circuit = QuantumCircuit(n_features)
for r in range(reps):
for i in range(n_features):
circuit.rz(0.0, i)
return circuit

def _create_pauli_feature_map(self, n_features: int, reps: int) -> Any:
"""Create Pauli feature map circuit."""
circuit = QuantumCircuit(n_features)
Expand Down
10 changes: 10 additions & 0 deletions src/superquantx/backends/simulator_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,16 @@ def feature_map_circuit(x):
for i in range(n_features - 1):
circuit = self.add_gate(circuit, 'CNOT', [i, i + 1])

elif feature_map == 'ZFeatureMap':
for i in range(n_features):
circuit = self.add_gate(circuit, 'RZ', i, [2.0 * x[i]])

elif feature_map in ('AngleEncoding', 'AmplitudeMap'):
# AmplitudeMap falls back to angle encoding on the pure
# simulator (full amplitude prep is backend-specific).
for i in range(n_features):
circuit = self.add_gate(circuit, 'RY', i, [x[i]])

else: # Default angle encoding
for i in range(n_features):
circuit = self.add_gate(circuit, 'RY', i, [x[i]])
Expand Down
Loading
Loading