summaryrefslogtreecommitdiff
path: root/sdil
diff options
context:
space:
mode:
Diffstat (limited to 'sdil')
-rw-r--r--sdil/physical_grid.py89
1 files changed, 85 insertions, 4 deletions
diff --git a/sdil/physical_grid.py b/sdil/physical_grid.py
index 2ef95e4..1d46714 100644
--- a/sdil/physical_grid.py
+++ b/sdil/physical_grid.py
@@ -245,6 +245,48 @@ class GridSquareLawImperfection:
return self.observed_rate(learning_rate, free_drops, free_drops)
+@dataclass(frozen=True)
+class AutozeroSampleHold:
+ """Nonideal local sample-and-hold used for hardware auto-zeroing.
+
+ The sampled input is the learning-circuit output while the free and
+ clamped edge voltages are equal. No device parameters are exposed to the
+ sampler or to the learning rule.
+ """
+
+ sample_gain: float = 1.0
+ pedestal_offset_v_per_s: float | Array = 0.0
+ sample_noise_standard_deviation_v_per_s: float = 0.0
+ refresh_interval_updates: int = 1
+
+ def __post_init__(self) -> None:
+ if not np.isfinite(self.sample_gain) or self.sample_gain < 0.0:
+ raise ValueError("sample gain must be finite and nonnegative")
+ if (
+ not np.isfinite(self.sample_noise_standard_deviation_v_per_s)
+ or self.sample_noise_standard_deviation_v_per_s < 0.0
+ ):
+ raise ValueError("sample noise must be finite and nonnegative")
+ if self.refresh_interval_updates < 1:
+ raise ValueError("refresh interval must be positive")
+
+ def sample(self, neutral_output: Array, rng: np.random.Generator) -> Array:
+ neutral = np.asarray(neutral_output, dtype=float)
+ offset = np.asarray(self.pedestal_offset_v_per_s, dtype=float)
+ try:
+ offset = np.broadcast_to(offset, neutral.shape)
+ except ValueError as error:
+ raise ValueError(
+ "sample-and-hold pedestal cannot broadcast to the edge vector"
+ ) from error
+ noise = rng.normal(
+ 0.0,
+ self.sample_noise_standard_deviation_v_per_s,
+ neutral.shape,
+ )
+ return self.sample_gain * neutral + offset + noise
+
+
@dataclass
class EdgePolynomialPredictor:
feature_center: Array
@@ -373,6 +415,8 @@ def train_grid_classifier(
method: str,
epochs: int,
predictor: EdgePolynomialPredictor | None = None,
+ autozero_sample_hold: AutozeroSampleHold | None = None,
+ autozero_seed: int = 0,
standard_nudging: float = 128.0 / 129.0,
standard_learning_time_seconds: float = 1.0e-3,
overclamp_nudging: float = 32.0 / 129.0,
@@ -388,11 +432,13 @@ def train_grid_classifier(
"constant",
"sdil",
"oracle_neutral",
+ "autozero_sdil",
"overclamp_clean",
"overclamp",
"overclamp_constant",
"overclamp_sdil",
"overclamp_oracle_neutral",
+ "overclamp_autozero_sdil",
}
if method not in allowed:
raise ValueError(f"unrecognized method {method}")
@@ -402,6 +448,11 @@ def train_grid_classifier(
"constant", "sdil", "overclamp_constant", "overclamp_sdil"
} and predictor is None:
raise ValueError(f"{method} requires a predictor")
+ autozero_methods = {
+ "autozero_sdil", "overclamp_autozero_sdil"
+ }
+ if method in autozero_methods and autozero_sample_hold is None:
+ autozero_sample_hold = AutozeroSampleHold()
gates = np.asarray(initial_gates, dtype=float).copy()
if gates.shape != (circuit.edge_count,):
raise ValueError("initial gate vector has the wrong shape")
@@ -419,6 +470,12 @@ def train_grid_classifier(
max_clamp_displacement = 0.0
local_updates = 0
clipped_updates = 0
+ autozero_samples = 0
+ autozero_updates_since_sample = 0
+ held_neutral_output: Array | None = None
+ autozero_error_sum_squared = 0.0
+ autozero_error_entries = 0
+ autozero_rng = np.random.default_rng(autozero_seed)
perfect_checkpoints = 0
completed_epochs = 0
@@ -438,6 +495,23 @@ def train_grid_classifier(
error = label - output_free
if label * error <= 0.0:
continue
+ free_drops = edge_voltage_drops(circuit, free_state)
+ if method in autozero_methods:
+ current_neutral_output = imperfection.observed_rate(
+ circuit.measured_learning_rate, free_drops, free_drops)
+ if (
+ held_neutral_output is None
+ or autozero_updates_since_sample
+ >= autozero_sample_hold.refresh_interval_updates
+ ):
+ held_neutral_output = autozero_sample_hold.sample(
+ current_neutral_output, autozero_rng)
+ autozero_samples += 1
+ autozero_updates_since_sample = 0
+ autozero_error_sum_squared += float(np.sum(np.square(
+ held_neutral_output - current_neutral_output)))
+ autozero_error_entries += circuit.edge_count
+ autozero_updates_since_sample += 1
if is_overclamp:
output_clamped = output_free + overclamp_nudging * (
target_magnitude * np.sign(error) - output_free)
@@ -461,20 +535,21 @@ def train_grid_classifier(
else clamped_cache[sample]),
)
clamped_cache[sample] = clamped_state
- free_drops = edge_voltage_drops(circuit, free_state)
clamped_drops = edge_voltage_drops(circuit, clamped_state)
ideal_rate = imperfection.ideal_rate(
circuit.measured_learning_rate, free_drops, clamped_drops)
observed_rate = imperfection.observed_rate(
circuit.measured_learning_rate, free_drops, clamped_drops)
- neutral_bias = imperfection.neutral_bias(
- circuit.measured_learning_rate, free_drops)
if method in {"clean", "overclamp_clean"}:
applied_rate = ideal_rate
elif method in {
"oracle_neutral", "overclamp_oracle_neutral"
}:
- applied_rate = observed_rate - neutral_bias
+ neutral_output = imperfection.observed_rate(
+ circuit.measured_learning_rate, free_drops, free_drops)
+ applied_rate = observed_rate - neutral_output
+ elif method in autozero_methods:
+ applied_rate = observed_rate - held_neutral_output
elif method in {
"constant", "sdil", "overclamp_constant", "overclamp_sdil"
}:
@@ -525,5 +600,11 @@ def train_grid_classifier(
"clamp_displacement_l2_time_v2_s": float(clamp_l2_time),
"max_abs_clamp_displacement_v": float(max_clamp_displacement),
"clipped_updates": clipped_updates,
+ "autozero_samples": autozero_samples,
+ "autozero_sample_fraction_per_update": float(
+ autozero_samples / local_updates if local_updates else 0.0),
+ "autozero_baseline_rmse_v_per_s": float(np.sqrt(
+ autozero_error_sum_squared / autozero_error_entries
+ )) if autozero_error_entries else None,
"trace": trace,
}