1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
|
"""Local square-law imperfection model for physical coupled learning.
The model follows the voltage-square learning rule and the three hardware
error sources discussed in Appendix C of Dillavou et al.
(arXiv:2505.22887v2): differential gain error, twin-state voltage mismatch,
and multiplier output offset. All predictors and updates are explicit local
NumPy operations with no autodiff path.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from sdil.physical_coupled import (
Array,
Circuit,
Task,
free_output,
overclamped_clean_rate,
standard_clean_rate,
task_errors,
)
def edge_voltage_drops(circuit: Circuit, output: float) -> Array:
"""Return the two locally measured positive edge-voltage drops."""
return np.asarray(
(output - circuit.low, circuit.high - output), dtype=float)
@dataclass(frozen=True)
class DifferentialSquareLawImperfection:
"""Fixed per-edge imperfections in the free/clamped measurement paths."""
free_gain: Array
clamped_gain: Array
free_input_offset_v: Array
clamped_input_offset_v: Array
multiplier_output_offset_v_per_s: Array
def __post_init__(self) -> None:
for value in (
self.free_gain,
self.clamped_gain,
self.free_input_offset_v,
self.clamped_input_offset_v,
self.multiplier_output_offset_v_per_s,
):
if np.asarray(value).shape != (2,):
raise ValueError("two-edge imperfection arrays must have shape (2,)")
@classmethod
def ideal(cls) -> "DifferentialSquareLawImperfection":
return cls(
free_gain=np.ones(2, dtype=float),
clamped_gain=np.ones(2, dtype=float),
free_input_offset_v=np.zeros(2, dtype=float),
clamped_input_offset_v=np.zeros(2, dtype=float),
multiplier_output_offset_v_per_s=np.zeros(2, dtype=float),
)
@classmethod
def sample_appendix_c(
cls,
seed: int,
*,
gain_standard_deviation: float = 0.01,
twin_mismatch_standard_deviation_v: float = 0.001,
multiplier_offset_standard_deviation_v_per_s: float = 2.3,
) -> "DifferentialSquareLawImperfection":
"""Sample a device using the component scales stated in Appendix C."""
if gain_standard_deviation < 0.0:
raise ValueError("gain standard deviation must be nonnegative")
if twin_mismatch_standard_deviation_v < 0.0:
raise ValueError("twin mismatch standard deviation must be nonnegative")
if multiplier_offset_standard_deviation_v_per_s < 0.0:
raise ValueError("multiplier offset scale must be nonnegative")
rng = np.random.default_rng(seed)
common_gain = 1.0 + rng.normal(0.0, gain_standard_deviation, size=2)
differential_gain = rng.normal(
0.0, gain_standard_deviation, size=2)
free_gain = common_gain + 0.5 * differential_gain
clamped_gain = common_gain - 0.5 * differential_gain
common_input_offset = rng.normal(
0.0, twin_mismatch_standard_deviation_v, size=2)
differential_input_offset = rng.normal(
0.0, twin_mismatch_standard_deviation_v, size=2)
free_input_offset = common_input_offset + 0.5 * differential_input_offset
clamped_input_offset = (
common_input_offset - 0.5 * differential_input_offset)
multiplier_offset = rng.normal(
0.0, multiplier_offset_standard_deviation_v_per_s, size=2)
return cls(
free_gain=free_gain,
clamped_gain=clamped_gain,
free_input_offset_v=free_input_offset,
clamped_input_offset_v=clamped_input_offset,
multiplier_output_offset_v_per_s=multiplier_offset,
)
def observed_rate(
self,
circuit: Circuit,
output_free: float,
output_clamped: float,
) -> Array:
free_drop = edge_voltage_drops(circuit, output_free)
clamped_drop = edge_voltage_drops(circuit, output_clamped)
measured_free = (
self.free_gain * free_drop + self.free_input_offset_v)
measured_clamped = (
self.clamped_gain * clamped_drop + self.clamped_input_offset_v)
return (
circuit.measured_learning_rate
* (np.square(measured_free) - np.square(measured_clamped))
+ self.multiplier_output_offset_v_per_s
)
def ideal_rate(
self,
circuit: Circuit,
output_free: float,
output_clamped: float,
) -> Array:
free_drop = edge_voltage_drops(circuit, output_free)
clamped_drop = edge_voltage_drops(circuit, output_clamped)
return circuit.measured_learning_rate * (
np.square(free_drop) - np.square(clamped_drop))
def neutral_bias(self, circuit: Circuit, output_free: float) -> Array:
return self.observed_rate(circuit, output_free, output_free)
def as_dict(self) -> dict:
return {
"free_gain": np.asarray(self.free_gain).tolist(),
"clamped_gain": np.asarray(self.clamped_gain).tolist(),
"free_input_offset_v": np.asarray(
self.free_input_offset_v).tolist(),
"clamped_input_offset_v": np.asarray(
self.clamped_input_offset_v).tolist(),
"multiplier_output_offset_v_per_s": np.asarray(
self.multiplier_output_offset_v_per_s).tolist(),
}
@dataclass
class LocalPolynomialPredictor:
"""Independent per-edge polynomial filters trained by normalized LMS."""
feature_center: Array
feature_scale: Array
coefficients: Array
@classmethod
def zeros(
cls,
feature_center: Array,
feature_scale: Array,
*,
degree: int,
) -> "LocalPolynomialPredictor":
if degree < 0:
raise ValueError("polynomial degree must be nonnegative")
center = np.asarray(feature_center, dtype=float)
scale = np.asarray(feature_scale, dtype=float)
if center.shape != (2,) or scale.shape != (2,):
raise ValueError("feature metadata must have shape (2,)")
if np.any(scale <= 0.0):
raise ValueError("feature scales must be positive")
return cls(
feature_center=center.copy(),
feature_scale=scale.copy(),
coefficients=np.zeros((2, degree + 1), dtype=float),
)
@property
def degree(self) -> int:
return int(self.coefficients.shape[1] - 1)
def features(self, local_state: Array) -> Array:
state = np.asarray(local_state, dtype=float)
if state.shape != (2,):
raise ValueError("local state must have shape (2,)")
normalized = (state - self.feature_center) / self.feature_scale
return np.stack(
[normalized ** power for power in range(self.degree + 1)], axis=1)
def predict(self, local_state: Array) -> Array:
return np.sum(self.coefficients * self.features(local_state), axis=1)
def update(
self,
local_state: Array,
neutral_measurement: Array,
learning_rate: float,
) -> Array:
measurement = np.asarray(neutral_measurement, dtype=float)
if measurement.shape != (2,):
raise ValueError("neutral measurement must have shape (2,)")
features = self.features(local_state)
prediction = np.sum(self.coefficients * features, axis=1)
residual = measurement - prediction
normalization = np.sum(features * features, axis=1, keepdims=True)
self.coefficients += (
learning_rate * residual[:, None] * features
/ np.maximum(normalization, 1e-12)
)
return residual
def copy(self) -> "LocalPolynomialPredictor":
return LocalPolynomialPredictor(
feature_center=self.feature_center.copy(),
feature_scale=self.feature_scale.copy(),
coefficients=self.coefficients.copy(),
)
def calibrate_polynomial_predictor(
predictor: LocalPolynomialPredictor,
local_states: Array,
neutral_measurements: Array,
*,
epochs: int,
learning_rate: float,
) -> int:
states = np.asarray(local_states, dtype=float)
measurements = np.asarray(neutral_measurements, dtype=float)
if states.ndim != 2 or states.shape[1] != 2:
raise ValueError("local states must have shape (observations, 2)")
if measurements.shape != states.shape:
raise ValueError("neutral measurements must match local states")
if epochs < 1:
raise ValueError("epochs must be positive")
observations = 0
for _ in range(epochs):
for state, measurement in zip(states, measurements):
predictor.update(state, measurement, learning_rate)
observations += 1
return observations
def fit_polynomial_predictor(
predictor: LocalPolynomialPredictor,
local_states: Array,
neutral_measurements: Array,
*,
ridge: float = 1e-12,
) -> int:
"""Fit independent per-edge sufficient statistics by local least squares."""
states = np.asarray(local_states, dtype=float)
measurements = np.asarray(neutral_measurements, dtype=float)
if states.ndim != 2 or states.shape[1] != 2:
raise ValueError("local states must have shape (observations, 2)")
if measurements.shape != states.shape:
raise ValueError("neutral measurements must match local states")
if ridge < 0.0:
raise ValueError("ridge must be nonnegative")
features = np.asarray([predictor.features(state) for state in states])
for edge in range(2):
design = features[:, edge, :]
gram = design.T @ design
rhs = design.T @ measurements[:, edge]
predictor.coefficients[edge] = np.linalg.solve(
gram + ridge * np.eye(gram.shape[0]), rhs)
return int(len(states))
def simulate_imperfect_alternating_tasks(
circuit: Circuit,
tasks: tuple[Task, Task],
imperfection: DifferentialSquareLawImperfection,
*,
method: str,
period_seconds: float,
cycles: int,
initial_gates: Array,
predictor: LocalPolynomialPredictor | None = None,
overclamp_nudging: float = 0.25,
overclamp_magnitude: float | None = None,
summary_cycles: int = 20,
record_history: bool = False,
) -> dict:
"""Alternate two tasks under a fixed local hardware imperfection."""
allowed = {
"raw",
"constant",
"sdil",
"oracle_neutral",
"clean",
"overclamp",
"overclamp_constant",
"overclamp_sdil",
"overclamp_oracle_neutral",
"overclamp_clean",
}
if method not in allowed:
raise ValueError(f"unrecognized method {method}")
if method in {
"constant", "sdil", "overclamp_constant", "overclamp_sdil"
} and predictor is None:
raise ValueError(f"{method} requires a predictor")
if len(tasks) != 2:
raise ValueError("exactly two alternating tasks are required")
if period_seconds <= 0.0 or cycles < 1:
raise ValueError("period and cycles must be positive")
gates = np.asarray(initial_gates, dtype=float).copy()
if gates.shape != (2,):
raise ValueError("initial gates must have shape (2,)")
active_predictor = predictor.copy() if predictor is not None else None
nominal_step = circuit.integration_step_seconds
half_steps = max(1, int(round(period_seconds / (2.0 * nominal_step))))
initial_error_scale = float(np.mean([
abs(task.label_voltage - free_output(
circuit, gates, task.input_voltage))
for task in tasks
]))
initial_error_scale = max(initial_error_scale, 1e-6)
combined_error_history = []
cycle_span_history = []
residual_neutral_history = []
rate_distortion_history = []
learning_on_time = 0.0
clamp_l2_time = 0.0
max_clamp_displacement = 0.0
clipped_updates = 0
is_overclamp = method.startswith("overclamp")
for _ in range(cycles):
half_endpoints = []
half_errors = []
for task in tasks:
for _ in range(half_steps):
if is_overclamp:
_, output_free, output_clamped = overclamped_clean_rate(
circuit,
gates,
task,
nudging=overclamp_nudging,
clamp_magnitude=overclamp_magnitude,
exact_target=True,
)
duration = nominal_step * abs(
task.label_voltage - output_free) / initial_error_scale
else:
_, output_free, output_clamped = standard_clean_rate(
circuit, gates, task)
duration = nominal_step
ideal_rate = imperfection.ideal_rate(
circuit, output_free, output_clamped)
observed_rate = imperfection.observed_rate(
circuit, output_free, output_clamped)
local_state = edge_voltage_drops(circuit, output_free)
neutral_bias = imperfection.neutral_bias(circuit, output_free)
if method in {"clean", "overclamp_clean"}:
applied_rate = ideal_rate
elif method in {"oracle_neutral", "overclamp_oracle_neutral"}:
applied_rate = observed_rate - neutral_bias
elif method in {
"constant", "sdil", "overclamp_constant", "overclamp_sdil"
}:
applied_rate = observed_rate - active_predictor.predict(
local_state)
else:
applied_rate = observed_rate
proposed = gates + duration * applied_rate
clipped = np.clip(
proposed, circuit.gate_minimum, circuit.gate_maximum)
clipped_updates += int(np.any(clipped != proposed))
gates = clipped
learning_on_time += duration
displacement = abs(output_clamped - output_free)
clamp_l2_time += duration * displacement * displacement
max_clamp_displacement = max(
max_clamp_displacement, displacement)
predicted_neutral = (
np.zeros(2, dtype=float)
if active_predictor is None
else active_predictor.predict(local_state)
)
residual_neutral_history.append(float(np.mean(
np.square(neutral_bias - predicted_neutral))))
rate_distortion_history.append(float(np.mean(
np.square(applied_rate - ideal_rate))))
half_endpoints.append(gates.copy())
half_errors.append(task_errors(circuit, gates, tasks))
combined_error_history.append(float(np.mean(half_errors)))
cycle_span_history.append(float(np.linalg.norm(
half_endpoints[1] - half_endpoints[0])))
summary_count = min(summary_cycles, cycles)
combined = np.asarray(combined_error_history[-summary_count:])
spans = np.asarray(cycle_span_history[-summary_count:])
result = {
"method": method,
"period_seconds": period_seconds,
"cycles": cycles,
"half_steps": half_steps,
"initial_gates": np.asarray(initial_gates, dtype=float).tolist(),
"final_gates": gates.tolist(),
"mean_combined_error": float(np.mean(combined)),
"std_combined_error": float(np.std(combined)),
"mean_cycle_span": float(np.mean(spans)),
"std_cycle_span": float(np.std(spans)),
"learning_on_time_seconds": float(learning_on_time),
"clamp_displacement_l2_time_v2_s": float(clamp_l2_time),
"max_abs_clamp_displacement_v": float(max_clamp_displacement),
"mean_residual_neutral_bias_mse": float(np.mean(
residual_neutral_history)),
"mean_applied_rate_distortion_mse": float(np.mean(
rate_distortion_history)),
"clipped_updates": clipped_updates,
}
if record_history:
result.update({
"combined_error_history": combined_error_history,
"cycle_span_history": cycle_span_history,
"residual_neutral_mse_history": residual_neutral_history,
"rate_distortion_mse_history": rate_distortion_history,
})
return result
|