pyqrack-cpu 1.76.0__py3-none-macosx_14_0_arm64.whl → 1.80.3__py3-none-macosx_14_0_arm64.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.
- pyqrack/__init__.py +2 -2
- pyqrack/qrack_ace_backend.py +44 -70
- pyqrack/qrack_circuit.py +51 -40
- pyqrack/qrack_neuron.py +35 -10
- pyqrack/qrack_neuron_torch_layer.py +182 -106
- pyqrack/qrack_simulator.py +269 -271
- pyqrack/qrack_system/qrack_lib/{libqrack_pinvoke.9.34.5.dylib → libqrack_pinvoke.9.35.2.dylib} +0 -0
- pyqrack/qrack_system/qrack_lib/libqrack_pinvoke.dylib +0 -0
- pyqrack/qrack_system/qrack_system.py +19 -12
- pyqrack/stats/load_quantized_data.py +1 -3
- pyqrack/stats/quantize_by_range.py +2 -6
- {pyqrack_cpu-1.76.0.dist-info → pyqrack_cpu-1.80.3.dist-info}/METADATA +3 -3
- pyqrack_cpu-1.80.3.dist-info/RECORD +23 -0
- pyqrack_cpu-1.76.0.dist-info/RECORD +0 -23
- {pyqrack_cpu-1.76.0.dist-info → pyqrack_cpu-1.80.3.dist-info}/LICENSE +0 -0
- {pyqrack_cpu-1.76.0.dist-info → pyqrack_cpu-1.80.3.dist-info}/WHEEL +0 -0
- {pyqrack_cpu-1.76.0.dist-info → pyqrack_cpu-1.80.3.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# (C) Daniel Strano and the Qrack contributors 2017-
|
|
1
|
+
# (C) Daniel Strano and the Qrack contributors 2017-2026. All rights reserved.
|
|
2
2
|
#
|
|
3
3
|
# Initial draft by Elara (OpenAI custom GPT)
|
|
4
4
|
# Refined and architecturally clarified by Dan Strano
|
|
@@ -6,6 +6,11 @@
|
|
|
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 random
|
|
12
|
+
import sys
|
|
13
|
+
|
|
9
14
|
_IS_TORCH_AVAILABLE = True
|
|
10
15
|
try:
|
|
11
16
|
import torch
|
|
@@ -14,82 +19,133 @@ try:
|
|
|
14
19
|
except ImportError:
|
|
15
20
|
_IS_TORCH_AVAILABLE = False
|
|
16
21
|
|
|
22
|
+
from .pauli import Pauli
|
|
17
23
|
from .qrack_neuron import QrackNeuron
|
|
24
|
+
from .qrack_simulator import QrackSimulator
|
|
18
25
|
from .neuron_activation_fn import NeuronActivationFn
|
|
19
26
|
|
|
20
|
-
from itertools import chain, combinations
|
|
21
27
|
|
|
28
|
+
# Parameter-shift rule
|
|
29
|
+
param_shift_eps = math.pi / 2
|
|
30
|
+
# Neuron angle initialization
|
|
31
|
+
init_phi = math.asin(0.5)
|
|
22
32
|
|
|
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
33
|
|
|
34
|
+
class QrackNeuronTorchFunction(Function if _IS_TORCH_AVAILABLE else object):
|
|
35
|
+
"""Static forward/backward/apply functions for QrackNeuronTorch"""
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
@staticmethod
|
|
38
|
+
def forward(ctx, x, neuron):
|
|
39
|
+
ctx.neuron = neuron
|
|
40
|
+
ctx.simulator = neuron.simulator
|
|
41
|
+
ctx.save_for_backward(x)
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
neuron(
|
|
35
|
-
"""
|
|
43
|
+
# Baseline probability BEFORE applying this neuron's unitary
|
|
44
|
+
pre_prob = neuron.simulator.prob(neuron.target)
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self.neuron = neuron
|
|
40
|
-
|
|
41
|
-
def forward(self, x):
|
|
42
|
-
neuron = self.neuron
|
|
46
|
+
angles = x.detach().cpu().numpy() if x.requires_grad else x.numpy()
|
|
47
|
+
neuron.set_angles(angles)
|
|
43
48
|
neuron.predict(True, False)
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class QrackNeuronFunction(Function if _IS_TORCH_AVAILABLE else object):
|
|
49
|
-
"""Static forward/backward/apply functions for QrackTorchNeuron"""
|
|
50
|
-
|
|
51
|
-
@staticmethod
|
|
52
|
-
def forward(ctx, neuron):
|
|
53
|
-
# Save for backward
|
|
54
|
-
ctx.neuron = neuron
|
|
50
|
+
# Probability AFTER applying this neuron's unitary
|
|
51
|
+
post_prob = neuron.simulator.prob(neuron.target)
|
|
52
|
+
ctx.post_prob = post_prob
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
final_prob = neuron.simulator.prob(neuron.target)
|
|
59
|
-
ctx.delta = final_prob - init_prob
|
|
54
|
+
delta = math.asin(post_prob) - math.asin(pre_prob)
|
|
55
|
+
ctx.delta = delta
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if _IS_TORCH_AVAILABLE
|
|
64
|
-
else ctx.delta
|
|
65
|
-
)
|
|
57
|
+
# Return shape: (1,)
|
|
58
|
+
return x.new_tensor([delta])
|
|
66
59
|
|
|
67
60
|
@staticmethod
|
|
68
61
|
def backward(ctx, grad_output):
|
|
62
|
+
(x,) = ctx.saved_tensors
|
|
69
63
|
neuron = ctx.neuron
|
|
64
|
+
neuron.set_simulator(ctx.simulator)
|
|
65
|
+
post_prob = ctx.post_prob
|
|
66
|
+
|
|
67
|
+
angles = x.detach().cpu().numpy() if x.requires_grad else x.numpy()
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
# Restore simulator to state BEFORE this neuron's unitary
|
|
70
|
+
neuron.set_angles(angles)
|
|
72
71
|
neuron.unpredict()
|
|
73
|
-
|
|
74
|
-
reverse_delta = pre_unpredict - post_unpredict
|
|
72
|
+
pre_sim = neuron.simulator
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
grad_x = torch.zeros_like(x)
|
|
75
|
+
|
|
76
|
+
for i in range(x.shape[0]):
|
|
77
|
+
angle = angles[i]
|
|
78
|
+
|
|
79
|
+
# θ + π/2
|
|
80
|
+
angles[i] = angle + param_shift_eps
|
|
81
|
+
neuron.set_angles(angles)
|
|
82
|
+
neuron.simulator = pre_sim.clone()
|
|
83
|
+
neuron.predict(True, False)
|
|
84
|
+
p_plus = neuron.simulator.prob(neuron.target)
|
|
85
|
+
|
|
86
|
+
# θ − π/2
|
|
87
|
+
angles[i] = angle - param_shift_eps
|
|
88
|
+
neuron.set_angles(angles)
|
|
89
|
+
neuron.simulator = pre_sim.clone()
|
|
90
|
+
neuron.predict(True, False)
|
|
91
|
+
p_minus = neuron.simulator.prob(neuron.target)
|
|
92
|
+
|
|
93
|
+
# Parameter-shift gradient
|
|
94
|
+
grad_x[i] = 0.5 * (p_plus - p_minus)
|
|
95
|
+
|
|
96
|
+
angles[i] = angle
|
|
97
|
+
|
|
98
|
+
# Restore simulator
|
|
99
|
+
neuron.set_simulator(pre_sim)
|
|
100
|
+
|
|
101
|
+
# Apply chain rule and upstream gradient
|
|
102
|
+
grad_x *= grad_output[0] / math.sqrt(max(1.0 - post_prob * post_prob, 1e-6))
|
|
103
|
+
|
|
104
|
+
return grad_x, None
|
|
77
105
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
|
|
107
|
+
class QrackNeuronTorch(nn.Module if _IS_TORCH_AVAILABLE else object):
|
|
108
|
+
"""Torch wrapper for QrackNeuron
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
neuron(QrackNeuron): QrackNeuron backing this torch wrapper
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, neuron, x):
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.neuron = neuron
|
|
117
|
+
self.weights = nn.Parameter(x)
|
|
118
|
+
|
|
119
|
+
def forward(self):
|
|
120
|
+
return QrackNeuronTorchFunction.apply(self.weights, self.neuron)
|
|
81
121
|
|
|
82
122
|
|
|
83
123
|
class QrackNeuronTorchLayer(nn.Module if _IS_TORCH_AVAILABLE else object):
|
|
84
|
-
"""Torch layer wrapper for QrackNeuron (with
|
|
124
|
+
"""Torch layer wrapper for QrackNeuron (with maximally expressive set of neurons between inputs and outputs)
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
simulator (QrackSimulator): Prototype simulator that batching copies to use with QrackNeuron instances
|
|
128
|
+
simulators (list[QrackSimulator]): In-flight copies of prototype simulator corresponding to batch count
|
|
129
|
+
input_indices (list[int], read-only): simulator qubit indices used as QrackNeuron inputs
|
|
130
|
+
output_indices (list[int], read-only): simulator qubit indices used as QrackNeuron outputs
|
|
131
|
+
hidden_indices (list[int], read-only): simulator qubit indices used as QrackNeuron hidden inputs (in maximal superposition)
|
|
132
|
+
neurons (ModuleList[QrackNeuronTorch]): QrackNeuronTorch wrappers (for PyQrack QrackNeurons) in this layer, corresponding to weights
|
|
133
|
+
weights (ParameterList): List of tensors corresponding one-to-one with weights of list of neurons
|
|
134
|
+
apply_fn (Callable[Tensor, QrackNeuronTorch]): Corresponds to QrackNeuronTorchFunction.apply(x, neuron_wrapper) (or override with a custom implementation)
|
|
135
|
+
backward_fn (Callable[Tensor, Tensor]): Corresponds to QrackNeuronTorchFunction._backward(x, neuron_wrapper) (or override with a custom implementation)
|
|
136
|
+
"""
|
|
85
137
|
|
|
86
138
|
def __init__(
|
|
87
139
|
self,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
input_qubits,
|
|
141
|
+
output_qubits,
|
|
142
|
+
hidden_qubits=None,
|
|
143
|
+
lowest_combo_count=0,
|
|
144
|
+
highest_combo_count=2,
|
|
91
145
|
activation=int(NeuronActivationFn.Generalized_Logistic),
|
|
146
|
+
dtype=torch.float if _IS_TORCH_AVAILABLE else float,
|
|
92
147
|
parameters=None,
|
|
148
|
+
**kwargs
|
|
93
149
|
):
|
|
94
150
|
"""
|
|
95
151
|
Initialize a QrackNeuron layer for PyTorch with a power set of neurons connecting inputs to outputs.
|
|
@@ -97,74 +153,94 @@ class QrackNeuronTorchLayer(nn.Module if _IS_TORCH_AVAILABLE else object):
|
|
|
97
153
|
|
|
98
154
|
Args:
|
|
99
155
|
sim (QrackSimulator): Simulator into which predictor features are loaded
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
input_qubits (int): Count of inputs (1 per qubit)
|
|
157
|
+
output_qubits (int): Count of outputs (1 per qubit)
|
|
158
|
+
hidden_qubits (int): Count of "hidden" inputs (1 per qubit, always initialized to |+>, suggested to be same a highest_combo_count)
|
|
159
|
+
lowest_combo_count (int): Lowest combination count of input qubits iterated (0 is bias)
|
|
160
|
+
highest_combo_count (int): Highest combination count of input qubits iterated
|
|
102
161
|
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
|
|
162
|
+
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
163
|
"""
|
|
105
164
|
super(QrackNeuronTorchLayer, self).__init__()
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
]
|
|
165
|
+
if hidden_qubits is None:
|
|
166
|
+
hidden_qubits = highest_combo_count
|
|
167
|
+
self.simulator = QrackSimulator(input_qubits + hidden_qubits + output_qubits, **kwargs)
|
|
168
|
+
self.simulators = []
|
|
169
|
+
self.input_indices = list(range(input_qubits))
|
|
170
|
+
self.hidden_indices = list(range(input_qubits, input_qubits + hidden_qubits))
|
|
171
|
+
self.output_indices = list(
|
|
172
|
+
range(input_qubits + hidden_qubits, input_qubits + hidden_qubits + output_qubits)
|
|
125
173
|
)
|
|
174
|
+
self.activation = NeuronActivationFn(activation)
|
|
175
|
+
self.dtype = dtype
|
|
176
|
+
self.apply_fn = QrackNeuronTorchFunction.apply
|
|
126
177
|
|
|
127
|
-
#
|
|
178
|
+
# Create neurons from all input combinations, projecting to coherent output qubits
|
|
179
|
+
neurons = []
|
|
128
180
|
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)
|
|
136
|
-
)
|
|
137
|
-
param_count += p_count
|
|
138
|
-
|
|
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
|
-
)
|
|
144
|
-
|
|
145
|
-
def forward(self, _):
|
|
146
|
-
# Assume quantum outputs should overwrite the simulator state
|
|
147
181
|
for output_id in self.output_indices:
|
|
148
|
-
|
|
149
|
-
self.
|
|
182
|
+
for k in range(lowest_combo_count, highest_combo_count + 1):
|
|
183
|
+
for input_subset in itertools.combinations(self.input_indices, k):
|
|
184
|
+
p_count = 1 << len(input_subset)
|
|
185
|
+
angles = (
|
|
186
|
+
(
|
|
187
|
+
torch.tensor(
|
|
188
|
+
parameters[param_count : (param_count + p_count)], dtype=dtype
|
|
189
|
+
)
|
|
190
|
+
if parameters
|
|
191
|
+
else torch.zeros(p_count, dtype=dtype)
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
neurons.append(
|
|
195
|
+
QrackNeuronTorch(
|
|
196
|
+
QrackNeuron(self.simulator, input_subset, output_id, activation), angles
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
param_count += p_count
|
|
200
|
+
self.neurons = nn.ModuleList(neurons)
|
|
201
|
+
|
|
202
|
+
def forward(self, x):
|
|
203
|
+
B = x.shape[0]
|
|
204
|
+
x = x.view(B, -1)
|
|
205
|
+
|
|
206
|
+
self.simulators.clear()
|
|
207
|
+
|
|
208
|
+
self.simulator.reset_all()
|
|
209
|
+
# Prepare hidden predictors
|
|
210
|
+
for hidden_id in self.hidden_indices:
|
|
211
|
+
self.simulator.h(hidden_id)
|
|
212
|
+
# Prepare a maximally uncertain output state.
|
|
213
|
+
for output_id in self.output_indices:
|
|
150
214
|
self.simulator.h(output_id)
|
|
151
215
|
|
|
152
|
-
#
|
|
153
|
-
|
|
216
|
+
# Group neurons by output target once
|
|
217
|
+
by_out = {out: [] for out in self.output_indices}
|
|
154
218
|
for neuron_wrapper in self.neurons:
|
|
155
|
-
|
|
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
|
|
219
|
+
by_out[neuron_wrapper.neuron.target].append(neuron_wrapper)
|
|
162
220
|
|
|
163
|
-
|
|
164
|
-
for
|
|
165
|
-
self.
|
|
221
|
+
batch_rows = []
|
|
222
|
+
for b in range(B):
|
|
223
|
+
simulator = self.simulator.clone()
|
|
224
|
+
self.simulators.append(simulator)
|
|
225
|
+
|
|
226
|
+
for q, input_id in enumerate(self.input_indices):
|
|
227
|
+
simulator.r(Pauli.PauliY, math.pi * x[b, q].item(), input_id)
|
|
228
|
+
|
|
229
|
+
row = []
|
|
230
|
+
for out in self.output_indices:
|
|
231
|
+
phi = torch.tensor(init_phi, device=x.device, dtype=x.dtype)
|
|
232
|
+
|
|
233
|
+
for neuron_wrapper in by_out[out]:
|
|
234
|
+
neuron_wrapper.neuron.set_simulator(simulator)
|
|
235
|
+
phi += self.apply_fn(
|
|
236
|
+
neuron_wrapper.weights,
|
|
237
|
+
neuron_wrapper.neuron
|
|
238
|
+
).squeeze()
|
|
239
|
+
|
|
240
|
+
# Convert angle back to probability
|
|
241
|
+
p = torch.clamp(torch.sin(phi), min=0.0)
|
|
242
|
+
row.append(p)
|
|
166
243
|
|
|
167
|
-
|
|
168
|
-
outputs = [self.simulator.prob(output_id) for output_id in self.output_indices]
|
|
244
|
+
batch_rows.append(torch.stack(row))
|
|
169
245
|
|
|
170
|
-
return torch.
|
|
246
|
+
return torch.stack(batch_rows)
|