quantumflow-sdk 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.
- api/__init__.py +1 -0
- api/auth.py +208 -0
- api/main.py +403 -0
- api/models.py +137 -0
- api/routes/__init__.py +1 -0
- api/routes/auth_routes.py +234 -0
- api/routes/teleport_routes.py +415 -0
- db/__init__.py +15 -0
- db/crud.py +319 -0
- db/database.py +93 -0
- db/models.py +197 -0
- quantumflow/__init__.py +47 -0
- quantumflow/algorithms/__init__.py +48 -0
- quantumflow/algorithms/compression/__init__.py +7 -0
- quantumflow/algorithms/compression/amplitude_amplification.py +189 -0
- quantumflow/algorithms/compression/qft_compression.py +133 -0
- quantumflow/algorithms/compression/token_compression.py +261 -0
- quantumflow/algorithms/cryptography/__init__.py +6 -0
- quantumflow/algorithms/cryptography/qkd.py +205 -0
- quantumflow/algorithms/cryptography/qrng.py +231 -0
- quantumflow/algorithms/machine_learning/__init__.py +7 -0
- quantumflow/algorithms/machine_learning/qnn.py +276 -0
- quantumflow/algorithms/machine_learning/qsvm.py +249 -0
- quantumflow/algorithms/machine_learning/vqe.py +229 -0
- quantumflow/algorithms/optimization/__init__.py +7 -0
- quantumflow/algorithms/optimization/grover.py +223 -0
- quantumflow/algorithms/optimization/qaoa.py +251 -0
- quantumflow/algorithms/optimization/quantum_annealing.py +237 -0
- quantumflow/algorithms/utility/__init__.py +6 -0
- quantumflow/algorithms/utility/circuit_optimizer.py +194 -0
- quantumflow/algorithms/utility/error_correction.py +330 -0
- quantumflow/api/__init__.py +1 -0
- quantumflow/api/routes/__init__.py +4 -0
- quantumflow/api/routes/billing_routes.py +520 -0
- quantumflow/backends/__init__.py +33 -0
- quantumflow/backends/base_backend.py +184 -0
- quantumflow/backends/braket_backend.py +345 -0
- quantumflow/backends/ibm_backend.py +112 -0
- quantumflow/backends/simulator_backend.py +86 -0
- quantumflow/billing/__init__.py +25 -0
- quantumflow/billing/models.py +126 -0
- quantumflow/billing/stripe_service.py +619 -0
- quantumflow/core/__init__.py +12 -0
- quantumflow/core/entanglement.py +164 -0
- quantumflow/core/memory.py +147 -0
- quantumflow/core/quantum_backprop.py +394 -0
- quantumflow/core/quantum_compressor.py +309 -0
- quantumflow/core/teleportation.py +386 -0
- quantumflow/integrations/__init__.py +107 -0
- quantumflow/integrations/autogen_tools.py +501 -0
- quantumflow/integrations/crewai_agents.py +425 -0
- quantumflow/integrations/crewai_tools.py +407 -0
- quantumflow/integrations/langchain_memory.py +385 -0
- quantumflow/integrations/langchain_tools.py +366 -0
- quantumflow/integrations/mcp_server.py +575 -0
- quantumflow_sdk-0.1.0.dist-info/METADATA +190 -0
- quantumflow_sdk-0.1.0.dist-info/RECORD +60 -0
- quantumflow_sdk-0.1.0.dist-info/WHEEL +5 -0
- quantumflow_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- quantumflow_sdk-0.1.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Key Distribution (QKD).
|
|
3
|
+
|
|
4
|
+
Implements BB84 protocol for secure key exchange.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import secrets
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
10
|
+
import numpy as np
|
|
11
|
+
from qiskit import QuantumCircuit
|
|
12
|
+
|
|
13
|
+
from quantumflow.backends.base_backend import QuantumBackend, get_backend, BackendType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class QKDResult:
|
|
18
|
+
"""Result from QKD key generation."""
|
|
19
|
+
|
|
20
|
+
shared_key: str
|
|
21
|
+
key_length: int
|
|
22
|
+
raw_key_length: int
|
|
23
|
+
error_rate: float
|
|
24
|
+
is_secure: bool
|
|
25
|
+
alice_bases: str
|
|
26
|
+
bob_bases: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QKD:
|
|
30
|
+
"""
|
|
31
|
+
Quantum Key Distribution using BB84 Protocol.
|
|
32
|
+
|
|
33
|
+
Enables two parties (Alice and Bob) to generate a shared secret key
|
|
34
|
+
with information-theoretic security guaranteed by quantum mechanics.
|
|
35
|
+
|
|
36
|
+
Protocol:
|
|
37
|
+
1. Alice prepares qubits in random states (Z or X basis)
|
|
38
|
+
2. Bob measures in random bases
|
|
39
|
+
3. They compare bases publicly
|
|
40
|
+
4. Keep only matching-basis measurements
|
|
41
|
+
5. Check for eavesdropping via error rate
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> qkd = QKD()
|
|
45
|
+
>>> result = qkd.generate_key(key_length=128)
|
|
46
|
+
>>> print(f"Shared key: {result.shared_key[:32]}...")
|
|
47
|
+
>>> print(f"Secure: {result.is_secure}")
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
backend: BackendType | str = BackendType.AUTO,
|
|
53
|
+
error_threshold: float = 0.11,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize QKD.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
backend: Quantum backend
|
|
60
|
+
error_threshold: Max tolerable error rate (11% for BB84)
|
|
61
|
+
"""
|
|
62
|
+
self.backend = get_backend(backend)
|
|
63
|
+
self.error_threshold = error_threshold
|
|
64
|
+
self._connected = False
|
|
65
|
+
|
|
66
|
+
def _ensure_connected(self):
|
|
67
|
+
if not self._connected:
|
|
68
|
+
self.backend.connect()
|
|
69
|
+
self._connected = True
|
|
70
|
+
|
|
71
|
+
def generate_key(
|
|
72
|
+
self,
|
|
73
|
+
key_length: int = 256,
|
|
74
|
+
with_eavesdropper: bool = False,
|
|
75
|
+
) -> QKDResult:
|
|
76
|
+
"""
|
|
77
|
+
Generate a shared cryptographic key.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
key_length: Desired final key length in bits
|
|
81
|
+
with_eavesdropper: Simulate eavesdropping (for testing)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
QKDResult with shared key
|
|
85
|
+
"""
|
|
86
|
+
self._ensure_connected()
|
|
87
|
+
|
|
88
|
+
# Need ~4x raw bits due to basis mismatch and error correction
|
|
89
|
+
raw_length = key_length * 4
|
|
90
|
+
|
|
91
|
+
# Step 1: Alice generates random bits and bases
|
|
92
|
+
alice_bits = self._random_bits(raw_length)
|
|
93
|
+
alice_bases = self._random_bits(raw_length) # 0=Z, 1=X
|
|
94
|
+
|
|
95
|
+
# Step 2: Alice prepares qubits
|
|
96
|
+
alice_states = self._prepare_states(alice_bits, alice_bases)
|
|
97
|
+
|
|
98
|
+
# Step 3: (Optional) Eve intercepts
|
|
99
|
+
if with_eavesdropper:
|
|
100
|
+
eve_bases = self._random_bits(raw_length)
|
|
101
|
+
alice_states = self._eavesdrop(alice_states, eve_bases)
|
|
102
|
+
|
|
103
|
+
# Step 4: Bob chooses random bases and measures
|
|
104
|
+
bob_bases = self._random_bits(raw_length)
|
|
105
|
+
bob_bits = self._measure_states(alice_states, bob_bases)
|
|
106
|
+
|
|
107
|
+
# Step 5: Sifting - keep only matching bases
|
|
108
|
+
sifted_alice = []
|
|
109
|
+
sifted_bob = []
|
|
110
|
+
|
|
111
|
+
for i in range(raw_length):
|
|
112
|
+
if alice_bases[i] == bob_bases[i]:
|
|
113
|
+
sifted_alice.append(alice_bits[i])
|
|
114
|
+
sifted_bob.append(bob_bits[i])
|
|
115
|
+
|
|
116
|
+
# Step 6: Error estimation (use subset)
|
|
117
|
+
check_length = min(len(sifted_alice) // 4, 50)
|
|
118
|
+
errors = sum(
|
|
119
|
+
sifted_alice[i] != sifted_bob[i]
|
|
120
|
+
for i in range(check_length)
|
|
121
|
+
)
|
|
122
|
+
error_rate = errors / check_length if check_length > 0 else 0
|
|
123
|
+
|
|
124
|
+
# Remove check bits
|
|
125
|
+
final_alice = sifted_alice[check_length:]
|
|
126
|
+
final_bob = sifted_bob[check_length:]
|
|
127
|
+
|
|
128
|
+
# Step 7: Check security
|
|
129
|
+
is_secure = error_rate < self.error_threshold
|
|
130
|
+
|
|
131
|
+
# Create key (truncate to desired length)
|
|
132
|
+
shared_key = ''.join(str(b) for b in final_alice[:key_length])
|
|
133
|
+
|
|
134
|
+
return QKDResult(
|
|
135
|
+
shared_key=shared_key,
|
|
136
|
+
key_length=len(shared_key),
|
|
137
|
+
raw_key_length=raw_length,
|
|
138
|
+
error_rate=error_rate,
|
|
139
|
+
is_secure=is_secure,
|
|
140
|
+
alice_bases=''.join(str(b) for b in alice_bases[:20]) + "...",
|
|
141
|
+
bob_bases=''.join(str(b) for b in bob_bases[:20]) + "...",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _random_bits(self, n: int) -> list[int]:
|
|
145
|
+
"""Generate n random bits."""
|
|
146
|
+
return [secrets.randbelow(2) for _ in range(n)]
|
|
147
|
+
|
|
148
|
+
def _prepare_states(
|
|
149
|
+
self,
|
|
150
|
+
bits: list[int],
|
|
151
|
+
bases: list[int],
|
|
152
|
+
) -> list[tuple[int, int]]:
|
|
153
|
+
"""Prepare quantum states based on bits and bases."""
|
|
154
|
+
return list(zip(bits, bases))
|
|
155
|
+
|
|
156
|
+
def _measure_states(
|
|
157
|
+
self,
|
|
158
|
+
states: list[tuple[int, int]],
|
|
159
|
+
bases: list[int],
|
|
160
|
+
) -> list[int]:
|
|
161
|
+
"""Measure states in given bases using quantum circuit."""
|
|
162
|
+
results = []
|
|
163
|
+
|
|
164
|
+
for (bit, prep_basis), meas_basis in zip(states, bases):
|
|
165
|
+
circuit = QuantumCircuit(1, 1)
|
|
166
|
+
|
|
167
|
+
# Prepare state
|
|
168
|
+
if bit == 1:
|
|
169
|
+
circuit.x(0)
|
|
170
|
+
if prep_basis == 1: # X basis
|
|
171
|
+
circuit.h(0)
|
|
172
|
+
|
|
173
|
+
# Measure in basis
|
|
174
|
+
if meas_basis == 1: # X basis
|
|
175
|
+
circuit.h(0)
|
|
176
|
+
circuit.measure(0, 0)
|
|
177
|
+
|
|
178
|
+
# Execute
|
|
179
|
+
result = self.backend.execute(circuit, shots=1)
|
|
180
|
+
measured = int(list(result.counts.keys())[0])
|
|
181
|
+
results.append(measured)
|
|
182
|
+
|
|
183
|
+
return results
|
|
184
|
+
|
|
185
|
+
def _eavesdrop(
|
|
186
|
+
self,
|
|
187
|
+
states: list[tuple[int, int]],
|
|
188
|
+
eve_bases: list[int],
|
|
189
|
+
) -> list[tuple[int, int]]:
|
|
190
|
+
"""Simulate eavesdropper (introduces errors)."""
|
|
191
|
+
disturbed = []
|
|
192
|
+
|
|
193
|
+
for (bit, basis), eve_basis in zip(states, eve_bases):
|
|
194
|
+
if eve_basis != basis:
|
|
195
|
+
# Eve's measurement disturbs state (50% chance of error)
|
|
196
|
+
if secrets.randbelow(2) == 0:
|
|
197
|
+
bit = 1 - bit # Flip bit
|
|
198
|
+
disturbed.append((bit, basis))
|
|
199
|
+
|
|
200
|
+
return disturbed
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class BB84(QKD):
|
|
204
|
+
"""Alias for QKD (BB84 protocol)."""
|
|
205
|
+
pass
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Random Number Generator (QRNG).
|
|
3
|
+
|
|
4
|
+
Generates true random numbers using quantum mechanics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import numpy as np
|
|
10
|
+
from qiskit import QuantumCircuit
|
|
11
|
+
|
|
12
|
+
from quantumflow.backends.base_backend import QuantumBackend, get_backend, BackendType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class QRNGResult:
|
|
17
|
+
"""Result from quantum random number generation."""
|
|
18
|
+
|
|
19
|
+
bits: str
|
|
20
|
+
integer: int
|
|
21
|
+
float_value: float
|
|
22
|
+
n_qubits_used: int
|
|
23
|
+
entropy_bits: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QRNG:
|
|
27
|
+
"""
|
|
28
|
+
Quantum Random Number Generator.
|
|
29
|
+
|
|
30
|
+
Generates cryptographically secure random numbers using
|
|
31
|
+
quantum superposition and measurement.
|
|
32
|
+
|
|
33
|
+
Unlike classical PRNGs:
|
|
34
|
+
- Randomness is fundamental, not pseudo
|
|
35
|
+
- Information-theoretically unpredictable
|
|
36
|
+
- Certified by quantum mechanics
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> qrng = QRNG()
|
|
40
|
+
>>> bits = qrng.random_bits(128)
|
|
41
|
+
>>> number = qrng.random_int(0, 1000)
|
|
42
|
+
>>> value = qrng.random_float()
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
backend: BackendType | str = BackendType.AUTO,
|
|
48
|
+
max_qubits: int = 20,
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Initialize QRNG.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
backend: Quantum backend
|
|
55
|
+
max_qubits: Maximum qubits per generation
|
|
56
|
+
"""
|
|
57
|
+
self.backend = get_backend(backend)
|
|
58
|
+
self.max_qubits = max_qubits
|
|
59
|
+
self._connected = False
|
|
60
|
+
self._buffer: list[int] = []
|
|
61
|
+
|
|
62
|
+
def _ensure_connected(self):
|
|
63
|
+
if not self._connected:
|
|
64
|
+
self.backend.connect()
|
|
65
|
+
self._connected = True
|
|
66
|
+
|
|
67
|
+
def random_bits(self, n: int) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Generate n random bits.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
n: Number of bits to generate
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
String of '0' and '1' characters
|
|
76
|
+
"""
|
|
77
|
+
self._ensure_connected()
|
|
78
|
+
|
|
79
|
+
bits = []
|
|
80
|
+
|
|
81
|
+
while len(bits) < n:
|
|
82
|
+
# Generate batch of bits
|
|
83
|
+
batch_size = min(self.max_qubits, n - len(bits))
|
|
84
|
+
batch = self._generate_batch(batch_size)
|
|
85
|
+
bits.extend(batch)
|
|
86
|
+
|
|
87
|
+
return ''.join(str(b) for b in bits[:n])
|
|
88
|
+
|
|
89
|
+
def random_int(self, min_val: int = 0, max_val: int = 2**32 - 1) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Generate random integer in range [min_val, max_val].
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
min_val: Minimum value (inclusive)
|
|
95
|
+
max_val: Maximum value (inclusive)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Random integer
|
|
99
|
+
"""
|
|
100
|
+
range_size = max_val - min_val + 1
|
|
101
|
+
bits_needed = int(np.ceil(np.log2(range_size))) + 1
|
|
102
|
+
|
|
103
|
+
# Rejection sampling to avoid bias
|
|
104
|
+
while True:
|
|
105
|
+
bits = self.random_bits(bits_needed)
|
|
106
|
+
value = int(bits, 2)
|
|
107
|
+
if value < range_size:
|
|
108
|
+
return min_val + value
|
|
109
|
+
|
|
110
|
+
def random_float(self) -> float:
|
|
111
|
+
"""
|
|
112
|
+
Generate random float in [0, 1).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Random float with 53 bits of precision
|
|
116
|
+
"""
|
|
117
|
+
# IEEE 754 double has 53 bits of mantissa
|
|
118
|
+
bits = self.random_bits(53)
|
|
119
|
+
value = int(bits, 2)
|
|
120
|
+
return value / (2**53)
|
|
121
|
+
|
|
122
|
+
def random_bytes(self, n: int) -> bytes:
|
|
123
|
+
"""
|
|
124
|
+
Generate n random bytes.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
n: Number of bytes
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Random bytes
|
|
131
|
+
"""
|
|
132
|
+
bits = self.random_bits(n * 8)
|
|
133
|
+
return int(bits, 2).to_bytes(n, byteorder='big')
|
|
134
|
+
|
|
135
|
+
def random_gaussian(self, mean: float = 0.0, std: float = 1.0) -> float:
|
|
136
|
+
"""
|
|
137
|
+
Generate Gaussian distributed random number.
|
|
138
|
+
|
|
139
|
+
Uses Box-Muller transform.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
mean: Mean of distribution
|
|
143
|
+
std: Standard deviation
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Gaussian random number
|
|
147
|
+
"""
|
|
148
|
+
u1 = self.random_float()
|
|
149
|
+
u2 = self.random_float()
|
|
150
|
+
|
|
151
|
+
# Avoid log(0)
|
|
152
|
+
while u1 < 1e-10:
|
|
153
|
+
u1 = self.random_float()
|
|
154
|
+
|
|
155
|
+
# Box-Muller transform
|
|
156
|
+
z = np.sqrt(-2 * np.log(u1)) * np.cos(2 * np.pi * u2)
|
|
157
|
+
|
|
158
|
+
return mean + std * z
|
|
159
|
+
|
|
160
|
+
def random_choice(self, items: list) -> any:
|
|
161
|
+
"""
|
|
162
|
+
Randomly select an item from list.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
items: List to choose from
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Randomly selected item
|
|
169
|
+
"""
|
|
170
|
+
if not items:
|
|
171
|
+
raise ValueError("Cannot choose from empty list")
|
|
172
|
+
|
|
173
|
+
idx = self.random_int(0, len(items) - 1)
|
|
174
|
+
return items[idx]
|
|
175
|
+
|
|
176
|
+
def random_shuffle(self, items: list) -> list:
|
|
177
|
+
"""
|
|
178
|
+
Randomly shuffle a list.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
items: List to shuffle
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Shuffled copy of list
|
|
185
|
+
"""
|
|
186
|
+
result = items.copy()
|
|
187
|
+
n = len(result)
|
|
188
|
+
|
|
189
|
+
# Fisher-Yates shuffle
|
|
190
|
+
for i in range(n - 1, 0, -1):
|
|
191
|
+
j = self.random_int(0, i)
|
|
192
|
+
result[i], result[j] = result[j], result[i]
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
def _generate_batch(self, n_qubits: int) -> list[int]:
|
|
197
|
+
"""Generate batch of random bits using quantum circuit."""
|
|
198
|
+
circuit = QuantumCircuit(n_qubits, n_qubits)
|
|
199
|
+
|
|
200
|
+
# Put all qubits in superposition
|
|
201
|
+
circuit.h(range(n_qubits))
|
|
202
|
+
|
|
203
|
+
# Measure
|
|
204
|
+
circuit.measure(range(n_qubits), range(n_qubits))
|
|
205
|
+
|
|
206
|
+
# Execute with single shot
|
|
207
|
+
result = self.backend.execute(circuit, shots=1)
|
|
208
|
+
|
|
209
|
+
# Extract bits
|
|
210
|
+
bitstring = list(result.counts.keys())[0]
|
|
211
|
+
return [int(b) for b in bitstring]
|
|
212
|
+
|
|
213
|
+
def get_result(self, n_bits: int) -> QRNGResult:
|
|
214
|
+
"""
|
|
215
|
+
Generate random bits with full result details.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
n_bits: Number of bits
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
QRNGResult with all details
|
|
222
|
+
"""
|
|
223
|
+
bits = self.random_bits(n_bits)
|
|
224
|
+
|
|
225
|
+
return QRNGResult(
|
|
226
|
+
bits=bits,
|
|
227
|
+
integer=int(bits, 2) if bits else 0,
|
|
228
|
+
float_value=int(bits, 2) / (2**n_bits) if bits else 0.0,
|
|
229
|
+
n_qubits_used=min(self.max_qubits, n_bits),
|
|
230
|
+
entropy_bits=n_bits,
|
|
231
|
+
)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Neural Network (QNN).
|
|
3
|
+
|
|
4
|
+
Parameterized quantum circuits for machine learning tasks.
|
|
5
|
+
Integrates with Paper 2's quantum backpropagation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional, Callable
|
|
10
|
+
import numpy as np
|
|
11
|
+
from qiskit import QuantumCircuit
|
|
12
|
+
|
|
13
|
+
from quantumflow.backends.base_backend import QuantumBackend, get_backend, BackendType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class QNNResult:
|
|
18
|
+
"""Result from QNN training."""
|
|
19
|
+
|
|
20
|
+
final_weights: np.ndarray
|
|
21
|
+
loss_history: list[float]
|
|
22
|
+
accuracy: float
|
|
23
|
+
n_epochs: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QNN:
|
|
27
|
+
"""
|
|
28
|
+
Quantum Neural Network.
|
|
29
|
+
|
|
30
|
+
A parameterized quantum circuit that can be trained for:
|
|
31
|
+
- Binary classification
|
|
32
|
+
- Multi-class classification
|
|
33
|
+
- Regression
|
|
34
|
+
|
|
35
|
+
Architecture:
|
|
36
|
+
- Input encoding layer (amplitude or angle encoding)
|
|
37
|
+
- Variational layers (parameterized rotations + entanglement)
|
|
38
|
+
- Measurement layer
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> qnn = QNN(n_qubits=4, n_layers=3)
|
|
42
|
+
>>> result = qnn.fit(X_train, y_train, epochs=100)
|
|
43
|
+
>>> predictions = qnn.predict(X_test)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
n_qubits: int,
|
|
49
|
+
n_layers: int = 2,
|
|
50
|
+
backend: BackendType | str = BackendType.AUTO,
|
|
51
|
+
learning_rate: float = 0.1,
|
|
52
|
+
shots: int = 1024,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize QNN.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
n_qubits: Number of qubits
|
|
59
|
+
n_layers: Number of variational layers
|
|
60
|
+
backend: Quantum backend
|
|
61
|
+
learning_rate: Learning rate for optimization
|
|
62
|
+
shots: Measurement shots
|
|
63
|
+
"""
|
|
64
|
+
self.n_qubits = n_qubits
|
|
65
|
+
self.n_layers = n_layers
|
|
66
|
+
self.backend = get_backend(backend)
|
|
67
|
+
self.learning_rate = learning_rate
|
|
68
|
+
self.shots = shots
|
|
69
|
+
self._connected = False
|
|
70
|
+
|
|
71
|
+
# Initialize weights: 3 parameters per qubit per layer (rx, ry, rz)
|
|
72
|
+
self.n_params = n_qubits * n_layers * 3
|
|
73
|
+
self.weights = np.random.uniform(-np.pi, np.pi, self.n_params)
|
|
74
|
+
|
|
75
|
+
def _ensure_connected(self):
|
|
76
|
+
if not self._connected:
|
|
77
|
+
self.backend.connect()
|
|
78
|
+
self._connected = True
|
|
79
|
+
|
|
80
|
+
def fit(
|
|
81
|
+
self,
|
|
82
|
+
X: np.ndarray,
|
|
83
|
+
y: np.ndarray,
|
|
84
|
+
epochs: int = 100,
|
|
85
|
+
batch_size: int = 8,
|
|
86
|
+
) -> QNNResult:
|
|
87
|
+
"""
|
|
88
|
+
Train the QNN.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
X: Training features (n_samples, n_features)
|
|
92
|
+
y: Training labels (n_samples,)
|
|
93
|
+
epochs: Number of training epochs
|
|
94
|
+
batch_size: Mini-batch size
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
QNNResult with training history
|
|
98
|
+
"""
|
|
99
|
+
self._ensure_connected()
|
|
100
|
+
|
|
101
|
+
loss_history = []
|
|
102
|
+
|
|
103
|
+
for epoch in range(epochs):
|
|
104
|
+
epoch_loss = 0.0
|
|
105
|
+
|
|
106
|
+
# Mini-batch training
|
|
107
|
+
indices = np.random.permutation(len(X))
|
|
108
|
+
|
|
109
|
+
for i in range(0, len(X), batch_size):
|
|
110
|
+
batch_idx = indices[i:i + batch_size]
|
|
111
|
+
X_batch = X[batch_idx]
|
|
112
|
+
y_batch = y[batch_idx]
|
|
113
|
+
|
|
114
|
+
# Forward pass and gradient computation
|
|
115
|
+
batch_loss, gradients = self._compute_batch_gradient(X_batch, y_batch)
|
|
116
|
+
epoch_loss += batch_loss
|
|
117
|
+
|
|
118
|
+
# Update weights
|
|
119
|
+
self.weights -= self.learning_rate * gradients
|
|
120
|
+
|
|
121
|
+
epoch_loss /= (len(X) / batch_size)
|
|
122
|
+
loss_history.append(epoch_loss)
|
|
123
|
+
|
|
124
|
+
# Compute final accuracy
|
|
125
|
+
predictions = self.predict(X)
|
|
126
|
+
accuracy = np.mean(predictions == y)
|
|
127
|
+
|
|
128
|
+
return QNNResult(
|
|
129
|
+
final_weights=self.weights.copy(),
|
|
130
|
+
loss_history=loss_history,
|
|
131
|
+
accuracy=accuracy,
|
|
132
|
+
n_epochs=epochs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
136
|
+
"""
|
|
137
|
+
Make predictions.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
X: Input features (n_samples, n_features)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Predicted labels
|
|
144
|
+
"""
|
|
145
|
+
self._ensure_connected()
|
|
146
|
+
|
|
147
|
+
predictions = []
|
|
148
|
+
|
|
149
|
+
for x in X:
|
|
150
|
+
prob = self._forward(x)
|
|
151
|
+
predictions.append(1 if prob > 0.5 else 0)
|
|
152
|
+
|
|
153
|
+
return np.array(predictions)
|
|
154
|
+
|
|
155
|
+
def predict_proba(self, X: np.ndarray) -> np.ndarray:
|
|
156
|
+
"""
|
|
157
|
+
Predict class probabilities.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
X: Input features
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Probabilities for class 1
|
|
164
|
+
"""
|
|
165
|
+
self._ensure_connected()
|
|
166
|
+
|
|
167
|
+
probs = []
|
|
168
|
+
for x in X:
|
|
169
|
+
prob = self._forward(x)
|
|
170
|
+
probs.append(prob)
|
|
171
|
+
|
|
172
|
+
return np.array(probs)
|
|
173
|
+
|
|
174
|
+
def _forward(self, x: np.ndarray) -> float:
|
|
175
|
+
"""Forward pass for a single input."""
|
|
176
|
+
circuit = self._build_circuit(x, self.weights)
|
|
177
|
+
result = self.backend.execute(circuit, shots=self.shots)
|
|
178
|
+
|
|
179
|
+
# Probability of measuring |1⟩ on first qubit
|
|
180
|
+
ones_count = sum(
|
|
181
|
+
count for state, count in result.counts.items()
|
|
182
|
+
if state[-1] == '1'
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return ones_count / self.shots
|
|
186
|
+
|
|
187
|
+
def _build_circuit(
|
|
188
|
+
self,
|
|
189
|
+
x: np.ndarray,
|
|
190
|
+
weights: np.ndarray,
|
|
191
|
+
) -> QuantumCircuit:
|
|
192
|
+
"""Build QNN circuit."""
|
|
193
|
+
circuit = QuantumCircuit(self.n_qubits, 1, name="qnn")
|
|
194
|
+
|
|
195
|
+
# Input encoding
|
|
196
|
+
self._encode_input(circuit, x)
|
|
197
|
+
|
|
198
|
+
# Variational layers
|
|
199
|
+
param_idx = 0
|
|
200
|
+
for layer in range(self.n_layers):
|
|
201
|
+
# Single qubit rotations
|
|
202
|
+
for i in range(self.n_qubits):
|
|
203
|
+
circuit.rx(weights[param_idx], i)
|
|
204
|
+
param_idx += 1
|
|
205
|
+
circuit.ry(weights[param_idx], i)
|
|
206
|
+
param_idx += 1
|
|
207
|
+
circuit.rz(weights[param_idx], i)
|
|
208
|
+
param_idx += 1
|
|
209
|
+
|
|
210
|
+
# Entangling layer (ring topology)
|
|
211
|
+
for i in range(self.n_qubits):
|
|
212
|
+
circuit.cx(i, (i + 1) % self.n_qubits)
|
|
213
|
+
|
|
214
|
+
# Measure first qubit
|
|
215
|
+
circuit.measure(0, 0)
|
|
216
|
+
|
|
217
|
+
return circuit
|
|
218
|
+
|
|
219
|
+
def _encode_input(self, circuit: QuantumCircuit, x: np.ndarray):
|
|
220
|
+
"""Encode input features into quantum state."""
|
|
221
|
+
# Angle encoding: each feature -> rotation angle
|
|
222
|
+
for i in range(min(len(x), self.n_qubits)):
|
|
223
|
+
circuit.ry(x[i] * np.pi, i)
|
|
224
|
+
|
|
225
|
+
# Initialize unmapped qubits
|
|
226
|
+
for i in range(len(x), self.n_qubits):
|
|
227
|
+
circuit.h(i)
|
|
228
|
+
|
|
229
|
+
def _compute_batch_gradient(
|
|
230
|
+
self,
|
|
231
|
+
X_batch: np.ndarray,
|
|
232
|
+
y_batch: np.ndarray,
|
|
233
|
+
) -> tuple[float, np.ndarray]:
|
|
234
|
+
"""Compute gradient using parameter shift rule."""
|
|
235
|
+
batch_loss = 0.0
|
|
236
|
+
gradients = np.zeros_like(self.weights)
|
|
237
|
+
shift = np.pi / 2
|
|
238
|
+
|
|
239
|
+
for x, y in zip(X_batch, y_batch):
|
|
240
|
+
# Forward pass
|
|
241
|
+
pred = self._forward(x)
|
|
242
|
+
loss = (pred - y) ** 2
|
|
243
|
+
batch_loss += loss
|
|
244
|
+
|
|
245
|
+
# Parameter shift gradient
|
|
246
|
+
for i in range(len(self.weights)):
|
|
247
|
+
# Shift +
|
|
248
|
+
weights_plus = self.weights.copy()
|
|
249
|
+
weights_plus[i] += shift
|
|
250
|
+
circuit_plus = self._build_circuit(x, weights_plus)
|
|
251
|
+
result_plus = self.backend.execute(circuit_plus, shots=self.shots)
|
|
252
|
+
ones_plus = sum(
|
|
253
|
+
c for s, c in result_plus.counts.items() if s[-1] == '1'
|
|
254
|
+
)
|
|
255
|
+
pred_plus = ones_plus / self.shots
|
|
256
|
+
|
|
257
|
+
# Shift -
|
|
258
|
+
weights_minus = self.weights.copy()
|
|
259
|
+
weights_minus[i] -= shift
|
|
260
|
+
circuit_minus = self._build_circuit(x, weights_minus)
|
|
261
|
+
result_minus = self.backend.execute(circuit_minus, shots=self.shots)
|
|
262
|
+
ones_minus = sum(
|
|
263
|
+
c for s, c in result_minus.counts.items() if s[-1] == '1'
|
|
264
|
+
)
|
|
265
|
+
pred_minus = ones_minus / self.shots
|
|
266
|
+
|
|
267
|
+
# Gradient
|
|
268
|
+
d_pred = (pred_plus - pred_minus) / 2
|
|
269
|
+
d_loss = 2 * (pred - y) * d_pred
|
|
270
|
+
gradients[i] += d_loss
|
|
271
|
+
|
|
272
|
+
# Average over batch
|
|
273
|
+
batch_loss /= len(X_batch)
|
|
274
|
+
gradients /= len(X_batch)
|
|
275
|
+
|
|
276
|
+
return batch_loss, gradients
|