pyqrack-cpu-complex128 1.69.0__py3-none-win_amd64.whl → 1.78.3__py3-none-win_amd64.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.
@@ -6,6 +6,10 @@
6
6
  # Use of this source code is governed by an MIT-style license that can be
7
7
  # found in the LICENSE file or at https://opensource.org/licenses/MIT.
8
8
 
9
+ import itertools
10
+ import math
11
+ import sys
12
+
9
13
  _IS_TORCH_AVAILABLE = True
10
14
  try:
11
15
  import torch
@@ -14,80 +18,143 @@ try:
14
18
  except ImportError:
15
19
  _IS_TORCH_AVAILABLE = False
16
20
 
21
+ from .pauli import Pauli
17
22
  from .qrack_neuron import QrackNeuron
23
+ from .qrack_simulator import QrackSimulator
18
24
  from .neuron_activation_fn import NeuronActivationFn
19
25
 
20
- from itertools import chain, combinations
21
26
 
27
+ # Should be safe for 16-bit
28
+ angle_eps = math.pi * (2 ** -8)
22
29
 
23
- # From https://stackoverflow.com/questions/1482308/how-to-get-all-subsets-of-a-set-powerset#answer-1482316
24
- def powerset(iterable):
25
- "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3,) (1,2,3)"
26
- s = list(iterable)
27
- return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
28
30
 
31
+ if not _IS_TORCH_AVAILABLE:
32
+ class TorchContextMock(object):
33
+ def __init__(self):
34
+ pass
29
35
 
30
- class QrackTorchNeuron(nn.Module if _IS_TORCH_AVAILABLE else object):
31
- """Torch wrapper for QrackNeuron
36
+ def save_for_backward(self, *args):
37
+ self.saved_tensors = args
32
38
 
33
- Attributes:
34
- neuron(QrackNeuron): QrackNeuron backing this torch wrapper
35
- """
39
+ class QrackNeuronTorchFunction(Function if _IS_TORCH_AVAILABLE else object):
40
+ """Static forward/backward/apply functions for QrackNeuronTorch"""
36
41
 
37
- def __init__(self, neuron: QrackNeuron):
38
- super().__init__()
39
- self.neuron = neuron
42
+ if not _IS_TORCH_AVAILABLE:
43
+ @staticmethod
44
+ def apply(x, neuron_wrapper):
45
+ return forward(TorchContextMock(), x, neuron_wrapper)
40
46
 
41
- def forward(self, x):
42
- neuron = self.neuron
47
+ @staticmethod
48
+ def forward(ctx, x, neuron_wrapper):
49
+ ctx.neuron_wrapper = neuron_wrapper
50
+ ctx.save_for_backward(x)
51
+ neuron = neuron_wrapper.neuron
52
+
53
+ angles = (x.detach().cpu().numpy() if x.requires_grad else x.numpy()) if _IS_TORCH_AVAILABLE else x
54
+ neuron.set_angles(angles)
43
55
  neuron.predict(True, False)
56
+ post_prob = neuron.simulator.prob(neuron.target)
57
+ if _IS_TORCH_AVAILABLE:
58
+ post_prob = torch.tensor([post_prob], dtype=torch.float32, device=x.device)
44
59
 
45
- return neuron.simulator.prob(neuron.target)
60
+ return post_prob
46
61
 
62
+ @staticmethod
63
+ def _backward(x, neuron_wrapper):
64
+ neuron = neuron_wrapper.neuron
65
+ angles = (x.detach().cpu().numpy() if x.requires_grad else x.numpy()) if _IS_TORCH_AVAILABLE else x
47
66
 
48
- class QrackNeuronFunction(Function if _IS_TORCH_AVAILABLE else object):
49
- """Static forward/backward/apply functions for QrackTorchNeuron"""
67
+ # Uncompute
68
+ neuron.set_angles(angles)
69
+ neuron.unpredict()
70
+ pre_sim = neuron.simulator
71
+ pre_prob = pre_sim.prob(neuron.target)
50
72
 
51
- @staticmethod
52
- def forward(ctx, neuron):
53
- # Save for backward
54
- ctx.neuron = neuron
73
+ param_count = 1 << len(neuron.controls)
74
+ delta = [0.0] * param_count
75
+ for param in range(param_count):
76
+ angle = angles[param]
55
77
 
56
- init_prob = neuron.simulator.prob(neuron.target)
57
- neuron.predict(True, False)
58
- final_prob = neuron.simulator.prob(neuron.target)
59
- ctx.delta = final_prob - init_prob
78
+ # x + angle_eps
79
+ angles[param] = angle + angle_eps
80
+ neuron.set_angles(angles)
81
+ neuron.simulator = pre_sim.clone()
82
+ neuron.predict(True, False)
83
+ p_plus = neuron.simulator.prob(neuron.target)
84
+
85
+ # x - angle_eps
86
+ angles[param] = angle - angle_eps
87
+ neuron.set_angles(angles)
88
+ neuron.simulator = pre_sim.clone()
89
+ neuron.predict(True, False)
90
+ p_minus = neuron.simulator.prob(neuron.target)
91
+
92
+ # Central difference
93
+ delta[param] = (p_plus - p_minus) / (2 * angle_eps)
60
94
 
61
- return (
62
- torch.tensor([ctx.delta], dtype=torch.float32)
63
- if _IS_TORCH_AVAILABLE
64
- else ctx.delta
65
- )
95
+ angles[param] = angle
96
+
97
+ neuron.simulator = pre_sim
98
+
99
+ if _IS_TORCH_AVAILABLE:
100
+ delta = torch.tensor(delta, dtype=torch.float32, device=x.device)
101
+
102
+ return delta
66
103
 
67
104
  @staticmethod
68
105
  def backward(ctx, grad_output):
69
- neuron = ctx.neuron
106
+ (x,) = ctx.saved_tensors
107
+ neuron_wrapper = ctx.neuron_wrapper
108
+ delta = _backward(x, neuron_wrapper, grad_output)
109
+ if _IS_TORCH_AVAILABLE:
110
+ # grad_output: (O,)
111
+ # delta: (O, I)
112
+ grad_input = torch.matmul(grad_output, delta) # result: (I,)
113
+ else:
114
+ grad_input = [
115
+ sum(o * d for o, d in zip(grad_output, col))
116
+ for col in zip(*delta)
117
+ ]
70
118
 
71
- pre_unpredict = neuron.simulator.prob(neuron.output_id)
72
- neuron.unpredict()
73
- post_unpredict = neuron.simulator.prob(neuron.output_id)
74
- reverse_delta = pre_unpredict - post_unpredict
119
+ return grad_input, None
120
+
121
+ class QrackNeuronTorch(nn.Module if _IS_TORCH_AVAILABLE else object):
122
+ """Torch wrapper for QrackNeuron
75
123
 
76
- grad = reverse_delta - ctx.delta
124
+ Attributes:
125
+ neuron(QrackNeuron): QrackNeuron backing this torch wrapper
126
+ """
77
127
 
78
- return (
79
- torch.tensor([grad], dtype=torch.float32) if _IS_TORCH_AVAILABLE else grad
80
- )
128
+ def __init__(self, neuron):
129
+ super().__init__()
130
+ self.neuron = neuron
131
+
132
+ def forward(self, x):
133
+ return QrackNeuronTorchFunction.apply(x, self.neuron)
81
134
 
82
135
 
83
136
  class QrackNeuronTorchLayer(nn.Module if _IS_TORCH_AVAILABLE else object):
84
- """Torch layer wrapper for QrackNeuron (with power set of neurons between inputs and outputs)"""
137
+ """Torch layer wrapper for QrackNeuron (with maximally expressive set of neurons between inputs and outputs)
138
+
139
+ Attributes:
140
+ simulator (QrackSimulator): Prototype simulator that batching copies to use with QrackNeuron instances
141
+ simulators (list[QrackSimulator]): In-flight copies of prototype simulator corresponding to batch count
142
+ input_indices (list[int], read-only): simulator qubit indices used as QrackNeuron inputs
143
+ output_indices (list[int], read-only): simulator qubit indices used as QrackNeuron outputs
144
+ hidden_indices (list[int], read-only): simulator qubit indices used as QrackNeuron hidden inputs (in maximal superposition)
145
+ neurons (ModuleList[QrackNeuronTorch]): QrackNeuronTorch wrappers (for PyQrack QrackNeurons) in this layer, corresponding to weights
146
+ weights (ParameterList): List of tensors corresponding one-to-one with weights of list of neurons
147
+ apply_fn (Callable[Tensor, QrackNeuronTorch]): Corresponds to QrackNeuronTorchFunction.apply(x, neuron_wrapper) (or override with a custom implementation)
148
+ backward_fn (Callable[Tensor, Tensor]): Corresponds to QrackNeuronTorchFunction._backward(x, neuron_wrapper) (or override with a custom implementation)
149
+ """
85
150
 
86
151
  def __init__(
87
152
  self,
88
- simulator,
89
- input_indices,
90
- output_indices,
153
+ input_qubits,
154
+ output_qubits,
155
+ hidden_qubits=None,
156
+ lowest_combo_count=0,
157
+ highest_combo_count=2,
91
158
  activation=int(NeuronActivationFn.Generalized_Logistic),
92
159
  parameters=None,
93
160
  ):
@@ -97,74 +164,197 @@ class QrackNeuronTorchLayer(nn.Module if _IS_TORCH_AVAILABLE else object):
97
164
 
98
165
  Args:
99
166
  sim (QrackSimulator): Simulator into which predictor features are loaded
100
- input_indices (list[int]): List of input bits
101
- output_indices (list[int]): List of output bits
167
+ input_qubits (int): Count of inputs (1 per qubit)
168
+ output_qubits (int): Count of outputs (1 per qubit)
169
+ hidden_qubits (int): Count of "hidden" inputs (1 per qubit, always initialized to |+>, suggested to be same a highest_combo_count)
170
+ lowest_combo_count (int): Lowest combination count of input qubits iterated (0 is bias)
171
+ highest_combo_count (int): Highest combination count of input qubits iterated
102
172
  activation (int): Integer corresponding to choice of activation function from NeuronActivationFn
103
- parameters (list[float]): (Optional) Flat list of initial neuron parameters, corresponding to little-endian basis states of power set of input indices, repeated for each output index (with empty set being constant bias)
173
+ parameters (list[float]): (Optional) Flat list of initial neuron parameters, corresponding to little-endian basis states of input + hidden qubits, repeated for ascending combo count, repeated for each output index
104
174
  """
105
175
  super(QrackNeuronTorchLayer, self).__init__()
106
- self.simulator = simulator
107
- self.input_indices = input_indices
108
- self.output_indices = output_indices
176
+ if hidden_qubits is None:
177
+ hidden_qubits = highest_combo_count
178
+ self.simulator = QrackSimulator(input_qubits + hidden_qubits + output_qubits)
179
+ self.simulators = []
180
+ self.input_indices = list(range(input_qubits))
181
+ self.hidden_indices = list(range(input_qubits, input_qubits + hidden_qubits))
182
+ self.output_indices = list(range(input_qubits + hidden_qubits, input_qubits + hidden_qubits + output_qubits))
109
183
  self.activation = NeuronActivationFn(activation)
110
- self.fn = (
111
- QrackNeuronFunction.apply
112
- if _IS_TORCH_AVAILABLE
113
- else lambda x: QrackNeuronFunction.forward(object(), x)
114
- )
115
-
116
- # Create neurons from all powerset input combinations, projecting to coherent output qubits
117
- self.neurons = nn.ModuleList(
118
- [
119
- QrackTorchNeuron(
120
- QrackNeuron(simulator, list(input_subset), output_id, activation)
121
- )
122
- for input_subset in powerset(input_indices)
123
- for output_id in output_indices
124
- ]
125
- )
184
+ self.apply_fn = QrackNeuronTorchFunction.apply
185
+ self.backward_fn = QrackNeuronTorchFunction._backward
126
186
 
127
- # Set Qrack's internal parameters:
128
- param_count = 0
129
- for neuron_wrapper in self.neurons:
130
- neuron = neuron_wrapper.neuron
131
- p_count = 1 << len(neuron.controls)
132
- neuron.set_angles(
133
- parameters[param_count : (param_count + p_count + 1)]
134
- if parameters
135
- else ([0.0] * p_count)
187
+ # Create neurons from all input combinations, projecting to coherent output qubits
188
+ neurons = [
189
+ QrackNeuronTorch(
190
+ QrackNeuron(self.simulator, input_subset, output_id, activation)
136
191
  )
137
- param_count += p_count
192
+ for output_id in self.output_indices
193
+ for k in range(lowest_combo_count, highest_combo_count + 1)
194
+ for input_subset in itertools.combinations(self.input_indices + self.hidden_indices, k)
195
+ ]
196
+ self.neurons = nn.ModuleList(neurons) if _IS_TORCH_AVAILABLE else neurons
138
197
 
139
- self.weights = nn.ParameterList()
140
- for pid in range(param_count):
141
- self.weights.append(
142
- nn.Parameter(torch.tensor(parameters[pid] if parameters else 0.0))
143
- )
198
+ # Set Qrack's internal parameters:
199
+ if parameters:
200
+ param_count = 0
201
+ self.weights = nn.ParameterList() if _IS_TORCH_AVAILABLE else []
202
+ for neuron_wrapper in self.neurons:
203
+ neuron = neuron_wrapper.neuron
204
+ p_count = 1 << len(neuron.controls)
205
+ neuron.set_angles(parameters[param_count : (param_count + p_count)])
206
+ self.weights.append(
207
+ nn.Parameter(torch.tensor(parameters[param_count : (param_count + p_count)]))
208
+ if _IS_TORCH_AVAILABLE else parameters[param_count : (param_count + p_count)]
209
+ )
210
+ param_count += p_count
211
+ else:
212
+ self.weights = nn.ParameterList() if _IS_TORCH_AVAILABLE else []
213
+ for neuron_wrapper in self.neurons:
214
+ neuron = neuron_wrapper.neuron
215
+ p_count = 1 << len(neuron.controls)
216
+ self.weights.append(nn.Parameter(torch.zeros(p_count)) if _IS_TORCH_AVAILABLE else ([0.0] * p_count))
144
217
 
145
- def forward(self, _):
146
- # Assume quantum outputs should overwrite the simulator state
147
- for output_id in self.output_indices:
148
- if self.simulator.m(output_id):
149
- self.simulator.x(output_id)
150
- self.simulator.h(output_id)
218
+ def forward(self, x):
219
+ return QrackNeuronTorchLayerFunction.apply(x, self)
151
220
 
152
- # Set Qrack's internal parameters:
153
- param_count = 0
154
- for neuron_wrapper in self.neurons:
155
- neuron = neuron_wrapper.neuron
156
- p_count = 1 << len(neuron.controls)
157
- angles = [
158
- w.item() for w in self.weights[param_count : (param_count + p_count)]
159
- ]
160
- neuron.set_angles(angles)
161
- param_count += p_count
162
221
 
163
- # Assume quantum inputs already loaded into simulator state
164
- for neuron_wrapper in self.neurons:
165
- self.fn(neuron_wrapper.neuron)
222
+ class QrackNeuronTorchLayerFunction(Function if _IS_TORCH_AVAILABLE else object):
223
+ """Static forward/backward/apply functions for QrackNeuronTorch"""
166
224
 
167
- # These are classical views over quantum state; simulator still maintains full coherence
168
- outputs = [self.simulator.prob(output_id) for output_id in self.output_indices]
225
+ @staticmethod
226
+ def forward(ctx, x, neuron_layer):
227
+ # Save for backward
228
+ ctx.save_for_backward(x)
229
+ ctx.neuron_layer = neuron_layer
230
+
231
+ input_indices = neuron_layer.input_indices
232
+ hidden_indices = neuron_layer.hidden_indices
233
+ output_indices = neuron_layer.output_indices
234
+ simulators = neuron_layer.simulators
235
+ weights = neuron_layer.weights
236
+
237
+ if _IS_TORCH_AVAILABLE:
238
+ B = x.shape[0]
239
+ x = x.view(B, -1)
240
+ else:
241
+ B = len(x)
242
+
243
+ simulators.clear()
244
+ if _IS_TORCH_AVAILABLE:
245
+ for b in range(B):
246
+ simulator = neuron_layer.simulator.clone()
247
+ simulators.append(simulator)
248
+ for q, input_id in enumerate(input_indices):
249
+ simulator.r(Pauli.PauliY, math.pi * x[b, q].item(), q)
250
+ else:
251
+ for b in range(B):
252
+ simulator = neuron_layer.simulator.clone()
253
+ simulators.append(simulator)
254
+ for q, input_id in enumerate(input_indices):
255
+ simulator.r(Pauli.PauliY, math.pi * x[b][q], q)
256
+
257
+ y = [([0.0] * len(output_indices)) for _ in range(B)]
258
+ for b in range(B):
259
+ simulator = simulators[b]
260
+ # Prepare a maximally uncertain output state.
261
+ for output_id in output_indices:
262
+ simulator.h(output_id)
263
+ # Prepare hidden predictors
264
+ for h in hidden_indices:
265
+ simulator.h(h)
266
+
267
+ # Set Qrack's internal parameters:
268
+ for idx, neuron_wrapper in enumerate(neuron_layer.neurons):
269
+ neuron_wrapper.neuron.simulator = simulator
270
+ neuron_layer.apply_fn(weights[idx], neuron_wrapper)
271
+
272
+ for q, output_id in enumerate(output_indices):
273
+ y[b][q] = simulator.prob(output_id)
274
+
275
+ if _IS_TORCH_AVAILABLE:
276
+ y = torch.tensor(y, dtype=torch.float32, device=x.device)
277
+
278
+ return y
169
279
 
170
- return torch.tensor(outputs, dtype=torch.float32)
280
+ @staticmethod
281
+ def backward(ctx, grad_output):
282
+ (x,) = ctx.saved_tensors
283
+ neuron_layer = ctx.neuron_layer
284
+
285
+ input_indices = neuron_layer.input_indices
286
+ hidden_indices = neuron_layer.hidden_indices
287
+ output_indices = neuron_layer.output_indices
288
+ simulators = neuron_layer.simulators
289
+ neurons = neuron_layer.neurons
290
+ backward_fn = neuron_layer.backward_fn
291
+
292
+ input_count = len(input_indices)
293
+ output_count = len(output_indices)
294
+
295
+ if _IS_TORCH_AVAILABLE:
296
+ B = x.shape[0]
297
+ x = x.view(B, -1)
298
+ else:
299
+ B = len(x)
300
+
301
+ # Uncompute prediction
302
+ if _IS_TORCH_AVAILABLE:
303
+ delta = torch.zeros((B, output_count, input_count), dtype=torch.float32, device=x.device)
304
+ for b in range(B):
305
+ simulator = simulators[b]
306
+ for neuron_wrapper in neurons:
307
+ neuron = neuron_wrapper.neuron
308
+ neuron.simulator = simulator
309
+ angles = torch.tensor(neuron.get_angles(), dtype=torch.float32, device=x.device, requires_grad=True)
310
+ o = output_indices.index(neuron.target)
311
+ neuron_grad = backward_fn(angles, neuron_wrapper)
312
+ for idx, c in enumerate(neuron.controls):
313
+ if c not in input_indices:
314
+ continue
315
+ i = input_indices.index(c)
316
+ delta[b, o, i] += neuron_grad[idx]
317
+ else:
318
+ delta = [[[0.0] * input_count for _ in range(output_count)] for _ in range(B)]
319
+ for b in range(B):
320
+ simulator = simulators[b]
321
+ for neuron_wrapper in neurons:
322
+ neuron = neuron_wrapper.neuron
323
+ neuron.simulator = simulator
324
+ angles = neuron.get_angles()
325
+ o = output_indices.index(neuron.target)
326
+ neuron_grad = backward_fn(angles, neuron_wrapper)
327
+ for idx, c in enumerate(neuron.controls):
328
+ if c not in input_indices:
329
+ continue
330
+ i = input_indices.index(c)
331
+ delta[b][o][i] += neuron_grad[idx]
332
+
333
+ # Uncompute output state prep
334
+ for simulator in simulators:
335
+ for output_id in output_indices:
336
+ simulator.h(output_id)
337
+ for h in hidden_indices:
338
+ simulator.h(output_id)
339
+
340
+ if _IS_TORCH_AVAILABLE:
341
+ for b in range(B):
342
+ simulator = simulators[b]
343
+ for q, input_id in enumerate(input_indices):
344
+ simulator.r(Pauli.PauliY, -math.pi * x[b, q].item(), q)
345
+ else:
346
+ for b in range(B):
347
+ simulator = simulators[b]
348
+ for q, input_id in enumerate(input_indices):
349
+ simulator.r(Pauli.PauliY, -math.pi * x[b][q].item(), q)
350
+
351
+ if _IS_TORCH_AVAILABLE:
352
+ grad_input = torch.matmul(grad_output.view(B, 1, -1), delta).view_as(x)
353
+ else:
354
+ grad_input = [[0.0] * output_count for _ in range(B)]
355
+ for b in range(B):
356
+ for o in range(output_indices):
357
+ for i in range(input_indices):
358
+ grad_input[b][o] += grad_output[b][o] * delta[b][o][i]
359
+
360
+ return grad_input, None