Skip to content

Commit 2e7ea92

Browse files
cahlenclaude
andcommitted
ci: fix RUF002 ambiguous-unicode lint errors (× → x, – → -)
CI rejected 494c7fa and 73e613a on RUF002 (ambiguous unicode characters in docstrings and strings). Local runs had passed earlier due to stale ruff cache; the actual lint is strict about: × (U+00D7 MULTIPLICATION SIGN) → replaced with x – (U+2013 EN DASH) → replaced with - Also applied ruff auto-fix on scripts/*.py (removed extraneous f-prefixes) and ruff format across src, tests, scripts. Functional behavior unchanged. Now local `ruff check src tests scripts && ruff format --check src tests scripts && mypy && pytest -m "not slow"` all pass, matching CI expectations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 494c7fa commit 2e7ea92

4 files changed

Lines changed: 129 additions & 69 deletions

File tree

scripts/horizon_probe.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Tests three promising Δs (the top-3 from the depth-56 search) at six depths:
55
60, 64, 68, 72, 76, 80. Each run is a tiny-budget training (100 k samples
6-
× 2 epochs). Total: 18 runs, ~1015 min on a 5090.
6+
x 2 epochs). Total: 18 runs, ~10-15 min on a 5090.
77
88
Outputs: docs/phase3b-results/horizon_probe.md with a per-depth-per-Δ table
99
plus a "deepest viable depth" recommendation.
@@ -20,7 +20,6 @@
2020

2121
from keeloq.neural.distinguisher import TrainingConfig, train
2222

23-
2423
# The three Δs that reached val-acc ≥ 0.65 at depth 56 (from delta_search.md).
2524
PROBE_DELTAS = [0x00000002, 0x00010000, 0x00800000]
2625
PROBE_DEPTHS = [60, 64, 68, 72, 76, 80]
@@ -33,8 +32,11 @@
3332

3433
def probe() -> dict:
3534
print("=" * 70, flush=True)
36-
print(f"Horizon probe: depths {PROBE_DEPTHS} × Δs {[f'0x{d:08x}' for d in PROBE_DELTAS]}", flush=True)
37-
print(f"Budget: 100 000 samples × 2 epochs per cell (depth=2, width=16)", flush=True)
35+
print(
36+
f"Horizon probe: depths {PROBE_DEPTHS} x Δs {[f'0x{d:08x}' for d in PROBE_DELTAS]}",
37+
flush=True,
38+
)
39+
print("Budget: 100 000 samples x 2 epochs per cell (depth=2, width=16)", flush=True)
3840
print("=" * 70, flush=True)
3941

4042
results: dict[int, dict[int, float]] = {}
@@ -61,7 +63,10 @@ def probe() -> dict:
6163
acc = res.final_val_accuracy
6264
elapsed = time.perf_counter() - t0
6365
results[depth][delta] = acc
64-
print(f" depth={depth:3d} Δ=0x{delta:08x} val_acc={acc:.4f} loss={res.final_loss:.4f} ({elapsed:.1f}s)", flush=True)
66+
print(
67+
f" depth={depth:3d} Δ=0x{delta:08x} val_acc={acc:.4f} loss={res.final_loss:.4f} ({elapsed:.1f}s)",
68+
flush=True,
69+
)
6570

6671
print("=" * 70, flush=True)
6772
print(f"Probe complete in {time.perf_counter() - t_all:.1f}s", flush=True)
@@ -70,9 +75,7 @@ def probe() -> dict:
7075

7176
def analyze(results: dict) -> dict:
7277
# Max accuracy achieved at each depth across all tested Δs:
73-
per_depth_best = {
74-
depth: max(cells.values()) for depth, cells in results.items()
75-
}
78+
per_depth_best = {depth: max(cells.values()) for depth, cells in results.items()}
7679
# Deepest depth that crossed the viability threshold at any Δ:
7780
viable_depths = [d for d, acc in per_depth_best.items() if acc >= VIABILITY_THRESHOLD]
7881
deepest_viable = max(viable_depths) if viable_depths else None
@@ -93,7 +96,7 @@ def write_markdown(results: dict, summary: dict, out: Path) -> None:
9396
lines.append(
9497
"Tests the signal cliff between depth 56 (known signal, d64.pt "
9598
f"trained here) and depth 88 (known collapse). Budget per cell: "
96-
f"100 k samples × 2 epochs, depth-2/width-16 tiny model. Viability "
99+
f"100 k samples x 2 epochs, depth-2/width-16 tiny model. Viability "
97100
f"threshold: val_acc ≥ {VIABILITY_THRESHOLD}.\n"
98101
)
99102
lines.append("## Results (val-accuracy at tiny-budget training)\n")
@@ -135,15 +138,21 @@ def main() -> None:
135138
results = probe()
136139
summary = analyze(results)
137140
print("\nSummary:", flush=True)
138-
print(json.dumps({
139-
"per_depth_best": {str(k): v for k, v in summary["per_depth_best"].items()},
140-
"deepest_viable_depth": summary["deepest_viable_depth"],
141-
"best_delta_at_deepest": (
142-
f"0x{summary['best_delta_at_deepest']:08x}"
143-
if summary["best_delta_at_deepest"] is not None
144-
else None
141+
print(
142+
json.dumps(
143+
{
144+
"per_depth_best": {str(k): v for k, v in summary["per_depth_best"].items()},
145+
"deepest_viable_depth": summary["deepest_viable_depth"],
146+
"best_delta_at_deepest": (
147+
f"0x{summary['best_delta_at_deepest']:08x}"
148+
if summary["best_delta_at_deepest"] is not None
149+
else None
150+
),
151+
},
152+
indent=2,
145153
),
146-
}, indent=2), flush=True)
154+
flush=True,
155+
)
147156

148157
out = Path("docs/phase3b-results/horizon_probe.md")
149158
write_markdown(results, summary, out)

scripts/horizon_probe_fine.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Fine-grained horizon probe at depths 57, 58, 59 (between known-signal 56
22
and probe-collapsed 60).
33
4-
If any cell at depths 5759 crosses the viability threshold (0.55), full-scale
4+
If any cell at depths 57-59 crosses the viability threshold (0.55), full-scale
55
training at that (depth, Δ) would give us a distinguisher for attacks at
66
~(depth + 8) rounds, pushing the coverage table past d64.pt.
77
@@ -16,7 +16,6 @@
1616

1717
from keeloq.neural.distinguisher import TrainingConfig, train
1818

19-
2019
# Top-8 Δs from the depth-56 search (val-acc ≥ 0.62).
2120
FINE_DELTAS = [
2221
0x00000002, # 0.688
@@ -34,7 +33,7 @@
3433

3534
def probe() -> dict:
3635
print("=" * 70, flush=True)
37-
print(f"Fine horizon probe: depths {FINE_DEPTHS} × top-8 Δs from depth-56 search", flush=True)
36+
print(f"Fine horizon probe: depths {FINE_DEPTHS} x top-8 Δs from depth-56 search", flush=True)
3837
print("=" * 70, flush=True)
3938
results: dict[int, dict[int, float]] = {}
4039
t_all = time.perf_counter()
@@ -43,9 +42,17 @@ def probe() -> dict:
4342
results[depth] = {}
4443
for delta in FINE_DELTAS:
4544
cfg = TrainingConfig(
46-
rounds=depth, delta=delta, n_samples=100_000,
47-
batch_size=1024, epochs=2, lr=2e-3, weight_decay=1e-5,
48-
seed=0, depth=2, width=16, val_samples=5000,
45+
rounds=depth,
46+
delta=delta,
47+
n_samples=100_000,
48+
batch_size=1024,
49+
epochs=2,
50+
lr=2e-3,
51+
weight_decay=1e-5,
52+
seed=0,
53+
depth=2,
54+
width=16,
55+
val_samples=5000,
4956
)
5057
_, res = train(cfg)
5158
acc = res.final_val_accuracy
@@ -72,7 +79,7 @@ def main() -> None:
7279
out = Path("docs/phase3b-results/horizon_probe_fine.md")
7380
lines = ["# Phase 3b Horizon Probe — Fine-Grained (57, 58, 59)\n"]
7481
lines.append(
75-
f"Top-8 Δs from depth-56 search, 100 k samples × 2 epochs. "
82+
f"Top-8 Δs from depth-56 search, 100 k samples x 2 epochs. "
7683
f"Viability threshold: val_acc ≥ {VIABILITY_THRESHOLD}.\n"
7784
)
7885
lines.append("## Results\n")

scripts/v2_experiment.py

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
Runs three sub-experiments to test whether the kernel-size-3 spatial-conv
44
DistinguisherSpatial architecture surfaces signal at depths that collapsed
5-
with the v1 1×1-conv Distinguisher:
5+
with the v1 1x1-conv Distinguisher:
66
77
1. Δ search at depth 56 (control — should reproduce v1's signal,
88
confirming v2 isn't broken).
@@ -27,7 +27,6 @@
2727
from keeloq.neural.differences import _default_candidate_set
2828
from keeloq.neural.distinguisher_v2 import DistinguisherSpatial
2929

30-
3130
# ---------- Standalone training loop (uses v2 architecture) ----------
3231

3332

@@ -42,14 +41,23 @@ def _set_seeds(seed: int) -> None:
4241
random.seed(seed)
4342

4443

45-
def _val_accuracy(model: nn.Module, rounds: int, delta: int, seed: int,
46-
n_samples: int = 5000, batch_size: int = 1024) -> float:
44+
def _val_accuracy(
45+
model: nn.Module,
46+
rounds: int,
47+
delta: int,
48+
seed: int,
49+
n_samples: int = 5000,
50+
batch_size: int = 1024,
51+
) -> float:
4752
model.train(False)
4853
correct, total = 0, 0
4954
with torch.no_grad():
5055
for batch in generate_pairs(
51-
rounds=rounds, delta=delta, n_samples=n_samples,
52-
seed=seed, batch_size=min(batch_size, n_samples),
56+
rounds=rounds,
57+
delta=delta,
58+
n_samples=n_samples,
59+
seed=seed,
60+
batch_size=min(batch_size, n_samples),
5361
):
5462
preds = (model(batch.pairs) >= 0.5).float()
5563
correct += (preds == batch.labels).sum().item()
@@ -84,8 +92,11 @@ def train_v2(
8492
for epoch in range(epochs):
8593
loss_sum, n_batches = 0.0, 0
8694
for batch in generate_pairs(
87-
rounds=rounds, delta=delta, n_samples=n_samples,
88-
seed=seed + epoch * 991, batch_size=batch_size,
95+
rounds=rounds,
96+
delta=delta,
97+
n_samples=n_samples,
98+
seed=seed + epoch * 991,
99+
batch_size=batch_size,
89100
):
90101
opt.zero_grad()
91102
preds = model(batch.pairs)
@@ -96,11 +107,13 @@ def train_v2(
96107
loss_sum += float(loss.item())
97108
n_batches += 1
98109
val_acc = _val_accuracy(model, rounds, delta, seed=seed + 1_000_000)
99-
history.append({
100-
"epoch": epoch,
101-
"train_loss": loss_sum / max(1, n_batches),
102-
"val_accuracy": val_acc,
103-
})
110+
history.append(
111+
{
112+
"epoch": epoch,
113+
"train_loss": loss_sum / max(1, n_batches),
114+
"val_accuracy": val_acc,
115+
}
116+
)
104117
return model, {
105118
"final_loss": history[-1]["train_loss"],
106119
"final_val_accuracy": history[-1]["val_accuracy"],
@@ -134,17 +147,25 @@ def search_delta_v2(
134147
results = []
135148
for i, delta in enumerate(uniq):
136149
_, res = train_v2(
137-
rounds=rounds, delta=delta,
138-
n_samples=tiny_budget_samples, batch_size=1024,
139-
epochs=tiny_budget_epochs, lr=2e-3, weight_decay=1e-5,
140-
seed=seed + i * 7919, depth=depth, width=width,
150+
rounds=rounds,
151+
delta=delta,
152+
n_samples=tiny_budget_samples,
153+
batch_size=1024,
154+
epochs=tiny_budget_epochs,
155+
lr=2e-3,
156+
weight_decay=1e-5,
157+
seed=seed + i * 7919,
158+
depth=depth,
159+
width=width,
141160
kernel_size=kernel_size,
142161
)
143-
results.append({
144-
"delta": delta,
145-
"val_accuracy": res["final_val_accuracy"],
146-
"training_loss_final": res["final_loss"],
147-
})
162+
results.append(
163+
{
164+
"delta": delta,
165+
"val_accuracy": res["final_val_accuracy"],
166+
"training_loss_final": res["final_loss"],
167+
}
168+
)
148169
results.sort(key=lambda c: c["val_accuracy"], reverse=True)
149170
return results
150171

@@ -166,57 +187,82 @@ def main() -> None:
166187
cands_56 = search_delta_v2(rounds=56, tiny_budget_samples=100_000, tiny_budget_epochs=2, seed=0)
167188
elapsed_56 = time.perf_counter() - t0
168189
best_56 = cands_56[0]
169-
lines.append(f"## Control: Δ search at depth 56 (v1 got best 0.688)\n")
190+
lines.append("## Control: Δ search at depth 56 (v1 got best 0.688)\n")
170191
lines.append(f"Wall clock: {elapsed_56:.1f}s — top 5:\n")
171192
lines.append("| Δ | val_acc | loss |\n|---|---:|---:|")
172193
for c in cands_56[:5]:
173-
lines.append(f"| 0x{c['delta']:08x} | {c['val_accuracy']:.4f} | {c['training_loss_final']:.4f} |")
174-
print(json.dumps({"experiment": "control_56", "best": best_56, "wall_s": elapsed_56}), flush=True)
194+
lines.append(
195+
f"| 0x{c['delta']:08x} | {c['val_accuracy']:.4f} | {c['training_loss_final']:.4f} |"
196+
)
197+
print(
198+
json.dumps({"experiment": "control_56", "best": best_56, "wall_s": elapsed_56}), flush=True
199+
)
175200

176201
# Experiment 2: Δ search at depth 88 (primary hypothesis).
177202
print("\n[v2-exp] Δ search at depth 88 (primary hypothesis)...", flush=True)
178203
t0 = time.perf_counter()
179204
cands_88 = search_delta_v2(rounds=88, tiny_budget_samples=100_000, tiny_budget_epochs=2, seed=0)
180205
elapsed_88 = time.perf_counter() - t0
181206
best_88 = cands_88[0]
182-
lines.append(f"\n## Primary: Δ search at depth 88 (v1 all < 0.517)\n")
207+
lines.append("\n## Primary: Δ search at depth 88 (v1 all < 0.517)\n")
183208
lines.append(f"Wall clock: {elapsed_88:.1f}s — top 10:\n")
184209
lines.append("| Δ | val_acc | loss |\n|---|---:|---:|")
185210
for c in cands_88[:10]:
186-
lines.append(f"| 0x{c['delta']:08x} | {c['val_accuracy']:.4f} | {c['training_loss_final']:.4f} |")
187-
print(json.dumps({"experiment": "primary_88", "best": best_88, "wall_s": elapsed_88}), flush=True)
211+
lines.append(
212+
f"| 0x{c['delta']:08x} | {c['val_accuracy']:.4f} | {c['training_loss_final']:.4f} |"
213+
)
214+
print(
215+
json.dumps({"experiment": "primary_88", "best": best_88, "wall_s": elapsed_88}), flush=True
216+
)
188217

189218
# Experiment 3 (conditional): Δ search at depth 120.
190219
print("\n[v2-exp] Δ search at depth 120 (stretch)...", flush=True)
191220
t0 = time.perf_counter()
192-
cands_120 = search_delta_v2(rounds=120, tiny_budget_samples=100_000, tiny_budget_epochs=2, seed=0)
221+
cands_120 = search_delta_v2(
222+
rounds=120, tiny_budget_samples=100_000, tiny_budget_epochs=2, seed=0
223+
)
193224
elapsed_120 = time.perf_counter() - t0
194225
best_120 = cands_120[0]
195-
lines.append(f"\n## Stretch: Δ search at depth 120 (v1 all < 0.515)\n")
226+
lines.append("\n## Stretch: Δ search at depth 120 (v1 all < 0.515)\n")
196227
lines.append(f"Wall clock: {elapsed_120:.1f}s — top 10:\n")
197228
lines.append("| Δ | val_acc | loss |\n|---|---:|---:|")
198229
for c in cands_120[:10]:
199-
lines.append(f"| 0x{c['delta']:08x} | {c['val_accuracy']:.4f} | {c['training_loss_final']:.4f} |")
200-
print(json.dumps({"experiment": "stretch_120", "best": best_120, "wall_s": elapsed_120}), flush=True)
230+
lines.append(
231+
f"| 0x{c['delta']:08x} | {c['val_accuracy']:.4f} | {c['training_loss_final']:.4f} |"
232+
)
233+
print(
234+
json.dumps({"experiment": "stretch_120", "best": best_120, "wall_s": elapsed_120}),
235+
flush=True,
236+
)
201237

202238
# Experiment 4 (conditional): if depth 88 has signal, full train.
203239
verdict_lines: list[str] = []
204-
verdict_lines.append(f"\n## Verdict\n")
240+
verdict_lines.append("\n## Verdict\n")
205241
if best_88["val_accuracy"] >= SIGNAL_THRESHOLD:
206242
verdict_lines.append(
207243
f"- Depth 88 best Δ=0x{best_88['delta']:08x} reached val-acc "
208244
f"{best_88['val_accuracy']:.4f} — **above the {SIGNAL_THRESHOLD} threshold**. "
209-
"Spatial conv architecture surfaces signal where v1's 1×1 version failed. "
245+
"Spatial conv architecture surfaces signal where v1's 1x1 version failed. "
210246
"Proceeding with a full-scale train at this Δ.\n"
211247
)
212-
print(f"\n[v2-exp] Depth 88 signal confirmed ({best_88['val_accuracy']:.4f}). "
213-
"Kicking off full train (10M samples × 20 epochs)...", flush=True)
248+
print(
249+
f"\n[v2-exp] Depth 88 signal confirmed ({best_88['val_accuracy']:.4f}). "
250+
"Kicking off full train (10M samples x 20 epochs)...",
251+
flush=True,
252+
)
214253
t0 = time.perf_counter()
215254
_, full_res = train_v2(
216-
rounds=88, delta=best_88["delta"],
217-
n_samples=10_000_000, batch_size=4096,
218-
epochs=20, lr=2e-3, weight_decay=1e-5,
219-
seed=1729, depth=5, width=256, kernel_size=3,
255+
rounds=88,
256+
delta=best_88["delta"],
257+
n_samples=10_000_000,
258+
batch_size=4096,
259+
epochs=20,
260+
lr=2e-3,
261+
weight_decay=1e-5,
262+
seed=1729,
263+
depth=5,
264+
width=256,
265+
kernel_size=3,
220266
)
221267
verdict_lines.append(
222268
f"- Full train: val_acc={full_res['final_val_accuracy']:.4f}, "
@@ -230,7 +276,7 @@ def main() -> None:
230276
f"{best_88['val_accuracy']:.4f} — **below the {SIGNAL_THRESHOLD} threshold**. "
231277
"Spatial conv architecture *also* fails to surface signal at depth 88. "
232278
"This tightens the negative result from 'v1 architecture fails' to "
233-
"'both 1×1 and spatial 3-tap architectures fail' — suggesting the "
279+
"'both 1x1 and spatial 3-tap architectures fail' — suggesting the "
234280
"signal horizon is a genuine property of KeeLoq's diffusion at these "
235281
"depths, not an artifact of any one architecture.\n"
236282
)

src/keeloq/neural/distinguisher_v2.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Spatial-convolution variant of the Gohr-style distinguisher for KeeLoq.
22
33
Hypothesis under test (see docs/phase3b-results/ambition_outcome.md §"What
4-
would push the frontier"): the v1 architecture's 1×1 convolutions blind the
4+
would push the frontier"): the v1 architecture's 1x1 convolutions blind the
55
model to bit-neighbor correlations, explaining its signal-horizon collapse
66
between depths 56 and 88. This variant reshapes the 64-bit input to
77
``(N, 2, 32)`` — two channels (c₀, c₁) over 32 spatial bit positions — and
@@ -75,9 +75,7 @@ def __init__(
7575
super().__init__()
7676
self.unpack = _BitUnpackSpatial()
7777
padding = kernel_size // 2
78-
self.embed = nn.Conv1d(
79-
2, width, kernel_size=kernel_size, padding=padding
80-
)
78+
self.embed = nn.Conv1d(2, width, kernel_size=kernel_size, padding=padding)
8179
self.blocks = nn.ModuleList(
8280
[_ResidualBlockSpatial(width, kernel_size) for _ in range(depth)]
8381
)

0 commit comments

Comments
 (0)