From 7a81f59cc943a9893c83d5ef6b47cc50baf69584 Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Sat, 29 Aug 2026 16:17:43 -0500 Subject: feat: add hardware autozero SDIL primitive --- sdil/physical_grid.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 4 deletions(-) (limited to 'sdil') 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, } -- cgit v1.2.3