RQ2-P3: bridge-federated-pool mechanism instrument + synthetic tests + calibration gate
Build the funnelling-mechanism instrument for the RQ2-P3 follow-up: a new
`bridge-federated-pool` topology drawing each circuit's bridge from a finite
willing-bridge pool (size B) under Zipf willingness skew (alpha), so bridge
concentration genuinely varies as a manipulated IV. Defensive measurement only:
synthetic seeds, no engine, no traffic, no confirmatory record read.
- assembler.py: add `bridge-federated-pool` branch + zipf_weights/weighted_draw
helpers; existing lead `bridge-federated` branch left untouched and
bit-reproducible (fresh bridge per seed).
- battery.py: enumerate_rq2p3_cells() = B{2,4,8} x alpha{0,1,2} + anchor B=50,
alpha=0 (separate fn; frozen 6-cell lead lattice unmutated).
- analysis/rq2p3_calibration.py: DRY synthetic-only §7 calibration gate; no
confirmatory data read. confirm_load_rq2.py/confirm.py/stats.py unchanged.
- tests/test_sor_rq2p3_pool.py: 9 synthetic tests (helpers + pool reuse/variance
+ lead branch reproducibility).
Calibration gate RAN: items 3 (monotonicity) & 4 (entropy) PASS; items 1 & 2 do
NOT match their naive-funnel wording because the ratified posterior yields a MIX
(rho>0 across the whole sweep incl anchor rho=+0.838; B=1 -> H=2.54 bits HIGH) --
the note-unique-bridge-artifact.md prediction, surfaced early. HARD HOLD: no
confirmatory battery, no prereg freeze; NEEDS-OPERATOR banner raised for the §7
item-1/2 re-word decision. Lead prereg SHA f22331a72e... untouched; worktree-only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
"""RQ2-P3 mechanism-instrument calibration gate (`rq2p3-mechanism-prereg.md` §7).
|
||||
|
||||
DRY, SYNTHETIC-ONLY calibration of the new ``bridge-federated-pool`` topology: it
|
||||
assembles circuits from harness-generated per-circuit seeds (NOT a confirmatory data
|
||||
dir), then reports the four §7 gate items. **No confirmatory record is read** — this
|
||||
is the same blind-safe discipline as the lead-paper loaders; the confirmatory battery
|
||||
stays HARD-HELD until the prereg is frozen.
|
||||
|
||||
Gate items (§7):
|
||||
1. Reproduce the lead degeneracy — ``B=50, alpha=0`` → ``c_i`` ≈ 1/C, near-constant,
|
||||
Spearman ρ inconclusive.
|
||||
2. Extreme ``B=1`` — all circuits share one bridge → ``c=1.0`` (a degenerate
|
||||
concentration extreme). NOTE: under the RATIFIED posterior a fully-shared bridge
|
||||
*maximizes* the observation-consistent anonymity set, so realized H comes out
|
||||
HIGH, not low — the mechanical "mix" the study exists to test. This is surfaced,
|
||||
not silently reconciled with the naive "low H" gloss.
|
||||
3. Monotonicity — mean top-3 concentration decreases in B and increases in alpha.
|
||||
4. Entropy calibration (inherited) — plug-in entropy of N equiprobable senders is
|
||||
exactly log2(N); Miller–Madow adds only the documented finite-N bias term.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
|
||||
from cmd_chat.sor.analysis.confirm_load_rq2 import (
|
||||
bridge_concentration,
|
||||
per_circuit_entropy,
|
||||
top_k_bridge_concentration,
|
||||
)
|
||||
from cmd_chat.sor.analysis.stats import miller_madow_entropy_bits, spearman
|
||||
from cmd_chat.sor.assembler import assemble
|
||||
from cmd_chat.sor.battery import Cell, derive_seed, enumerate_rq2p3_cells
|
||||
|
||||
R_DRY = 30 # runs/cell for the dry calibration (matches the prereg §6 R)
|
||||
C_DRY = 50 # circuits/run (matches C)
|
||||
|
||||
|
||||
def _run_circuit_seeds(cell_id: str, run_index: int, c: int = C_DRY) -> List[int]:
|
||||
"""Harness-side per-circuit seeds for the DRY pass (the confirmatory executor
|
||||
persists real per_circuit_seeds; this is calibration only). Deterministic from
|
||||
the frozen per-run seed rule so the calibration is reproducible."""
|
||||
run_seed = derive_seed(cell_id, run_index)
|
||||
return [int.from_bytes(hashlib.sha256(f"{run_seed}|circ|{j}".encode()).digest()[:8], "big")
|
||||
for j in range(c)]
|
||||
|
||||
|
||||
def _assemble_run(cell: Cell, run_index: int, c: int = C_DRY):
|
||||
return [assemble(cell, s) for s in _run_circuit_seeds(cell.cell_id, run_index, c)]
|
||||
|
||||
|
||||
def _pool_cell(b: int, alpha: float) -> Cell:
|
||||
return Cell("RQ2P3", f"RQ2P3/dry/B={b}/alpha={alpha}",
|
||||
{"bridge": "off", "topology": "bridge-federated-pool",
|
||||
"selector": "static", "pool_B": str(b), "pool_alpha": str(alpha)}, False)
|
||||
|
||||
|
||||
def cell_report(cell: Cell, r: int = R_DRY, c: int = C_DRY) -> Dict:
|
||||
"""Per-cell dry report: mean top-3 concentration over runs, pooled per-circuit
|
||||
(c_i, H_i) Spearman ρ, and the concentration spread."""
|
||||
per_run_top3: List[float] = []
|
||||
per_run_mean_h: List[float] = []
|
||||
conc_all: List[float] = []
|
||||
h_all: List[float] = []
|
||||
for ri in range(r):
|
||||
specs = _assemble_run(cell, ri, c)
|
||||
per_run_top3.append(top_k_bridge_concentration(specs, k=3))
|
||||
ent = per_circuit_entropy(specs)
|
||||
per_run_mean_h.append(statistics.fmean(ent))
|
||||
for ci, hi in zip(bridge_concentration(specs), ent):
|
||||
if ci is not None:
|
||||
conc_all.append(ci)
|
||||
h_all.append(hi)
|
||||
b = int(cell.factors["pool_B"])
|
||||
alpha = float(cell.factors["pool_alpha"])
|
||||
return {
|
||||
"B": b,
|
||||
"alpha": alpha,
|
||||
"mean_top3_concentration": statistics.fmean(per_run_top3),
|
||||
"concentration_stdev": statistics.pstdev(conc_all) if conc_all else 0.0,
|
||||
"mean_entropy_bits": statistics.fmean(per_run_mean_h),
|
||||
"spearman_rho_conc_vs_H": spearman(conc_all, h_all),
|
||||
"n_bridged_circuits": len(conc_all),
|
||||
}
|
||||
|
||||
|
||||
def calibration_gate(r: int = R_DRY, c: int = C_DRY) -> Dict:
|
||||
"""Run all four §7 gate items on the DRY pass and return a report dict."""
|
||||
sweep = [cr for cr in (cell_report(cell, r, c) for cell in enumerate_rq2p3_cells())]
|
||||
grid = {(cr["B"], cr["alpha"]): cr for cr in sweep}
|
||||
|
||||
# Item 1 — anchor B=50, alpha=0 reproduces the lead degeneracy.
|
||||
anchor = grid[(50, 0.0)]
|
||||
item1_pass = anchor["concentration_stdev"] < 1e-3 and abs(anchor["spearman_rho_conc_vs_H"]) < 0.05
|
||||
|
||||
# Item 2 — extreme B=1: all circuits share one bridge (degenerate concentration).
|
||||
ext = cell_report(_pool_cell(1, 0.0), r, c)
|
||||
lead_like = cell_report(_pool_cell(1_000_000, 0.0), r, c) # huge pool ≈ fresh bridge/circuit
|
||||
item2_conc_ok = abs(ext["mean_top3_concentration"] - 1.0) < 1e-9
|
||||
# The naive gate text says "low H"; the ratified posterior yields HIGH H. Surface it.
|
||||
item2_entropy_direction = "HIGH (mechanical mix)" if ext["mean_entropy_bits"] > lead_like["mean_entropy_bits"] else "low"
|
||||
item2_matches_naive_low_H = ext["mean_entropy_bits"] < lead_like["mean_entropy_bits"]
|
||||
|
||||
# Item 3 — monotonicity: mean top-3 concentration decreasing in B, increasing in alpha.
|
||||
dec_in_B = all(
|
||||
grid[(2, a)]["mean_top3_concentration"] >= grid[(4, a)]["mean_top3_concentration"] >= grid[(8, a)]["mean_top3_concentration"]
|
||||
for a in (0.0, 1.0, 2.0)
|
||||
)
|
||||
inc_in_alpha = all(
|
||||
grid[(b, 0.0)]["mean_top3_concentration"] <= grid[(b, 1.0)]["mean_top3_concentration"] <= grid[(b, 2.0)]["mean_top3_concentration"]
|
||||
for b in (2, 4, 8)
|
||||
)
|
||||
item3_pass = dec_in_B and inc_in_alpha
|
||||
|
||||
# Item 4 — entropy calibration (inherited): plug-in H of N equiprobable = log2(N) exactly.
|
||||
entropy_checks = []
|
||||
item4_pass = True
|
||||
for n in (2, 4, 8, 16, 50):
|
||||
mm = miller_madow_entropy_bits([1] * n)
|
||||
bias = (n - 1) / (2.0 * n * math.log(2.0))
|
||||
plugin = mm - bias
|
||||
ok = abs(plugin - math.log2(n)) < 1e-9
|
||||
item4_pass = item4_pass and ok
|
||||
entropy_checks.append({"N": n, "plugin_bits": plugin, "log2N": math.log2(n),
|
||||
"miller_madow_bits": mm, "plugin_equals_log2N": ok})
|
||||
|
||||
return {
|
||||
"schema": "sor-rq2p3-calibration/1",
|
||||
"dry_only": True,
|
||||
"no_confirmatory_data_read": True,
|
||||
"R": r, "C": c,
|
||||
"sweep": sweep,
|
||||
"gate": {
|
||||
"item1_reproduce_lead_degeneracy": {
|
||||
"anchor_B50_alpha0": anchor,
|
||||
"pass": item1_pass,
|
||||
},
|
||||
"item2_extreme_B1": {
|
||||
"mean_top3_concentration": ext["mean_top3_concentration"],
|
||||
"mean_entropy_bits": ext["mean_entropy_bits"],
|
||||
"fresh_bridge_reference_mean_entropy_bits": lead_like["mean_entropy_bits"],
|
||||
"concentration_degenerate_c_eq_1": item2_conc_ok,
|
||||
"entropy_direction_vs_naive": item2_entropy_direction,
|
||||
"matches_naive_low_H_expectation": item2_matches_naive_low_H,
|
||||
"NEEDS_OPERATOR": not item2_matches_naive_low_H,
|
||||
"note": (
|
||||
"Concentration extreme is as specified (c=1.0). But the ratified "
|
||||
"posterior makes a fully-shared bridge MAXIMIZE the anonymity set, so "
|
||||
"realized H is HIGH, contradicting the §7 gate-item-2 'low H' gloss. "
|
||||
"This is exactly the mechanical 'mix' (ρ>0) the study exists to test "
|
||||
"(see note-unique-bridge-artifact.md), surfaced early at calibration. "
|
||||
"Not silently reconciled: flagged for the operator's freeze decision."
|
||||
),
|
||||
},
|
||||
"item3_monotonicity": {
|
||||
"decreasing_in_B": dec_in_B,
|
||||
"increasing_in_alpha": inc_in_alpha,
|
||||
"pass": item3_pass,
|
||||
},
|
||||
"item4_entropy_calibration": {
|
||||
"checks": entropy_checks,
|
||||
"pass": item4_pass,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(calibration_gate(), indent=2, sort_keys=True))
|
||||
sys.exit(0)
|
||||
@@ -132,6 +132,36 @@ def _bridge_label(seed: int) -> str:
|
||||
return f"bridge#{hashlib.sha256(f'sor-bridge|{seed}'.encode()).hexdigest()[:8]}"
|
||||
|
||||
|
||||
def zipf_weights(pool_size: int, alpha: float) -> List[float]:
|
||||
"""Deterministic Zipf willingness weights over a finite willing-bridge POOL of
|
||||
``pool_size`` bridges: rank ``r`` (1..B) gets mass ∝ ``1/r**alpha``, normalized to
|
||||
sum 1. ``alpha = 0`` → uniform willingness; larger ``alpha`` → heavier skew toward
|
||||
the most-willing bridges (higher realized concentration). Pure function of
|
||||
``(pool_size, alpha)`` — both cell-level factors — so the willingness profile is
|
||||
fixed within a run (RQ2-P3 mechanism prereg §3)."""
|
||||
if pool_size < 1:
|
||||
raise ValueError(f"pool_size must be >= 1, got {pool_size}")
|
||||
raw = [1.0 / (r ** alpha) for r in range(1, pool_size + 1)]
|
||||
total = sum(raw)
|
||||
return [w / total for w in raw]
|
||||
|
||||
|
||||
def weighted_draw(digest_hex: str, weights: List[float]) -> int:
|
||||
"""Deterministic index in ``[0, len(weights))`` by CDF inversion of a SHA-256
|
||||
hex digest: the first 8 bytes map to ``u ∈ [0, 1)`` and we return the first bin
|
||||
whose cumulative weight exceeds ``u``. Deterministic from ``digest_hex`` (a
|
||||
per-circuit hash), so a circuit's willing bridge is reproducible from its seed."""
|
||||
if not weights:
|
||||
raise ValueError("weights must be non-empty")
|
||||
u = int(digest_hex[:16], 16) / float(1 << 64)
|
||||
cum = 0.0
|
||||
for i, w in enumerate(weights):
|
||||
cum += w
|
||||
if u < cum:
|
||||
return i
|
||||
return len(weights) - 1
|
||||
|
||||
|
||||
def _house_nodes(seed: int, pool: int, take: int, house: str, salt: int = 0) -> List[str]:
|
||||
idx = select_path(SorRng(seed ^ salt), pool, take)
|
||||
return [f"{house}#{i}" for i in idx]
|
||||
@@ -201,6 +231,27 @@ def assemble(
|
||||
]
|
||||
houses = (HOUSES[0], HOUSES[1])
|
||||
bridge_present = True
|
||||
elif topo == "bridge-federated-pool":
|
||||
# RQ2-P3 mechanism instrument (rq2p3-mechanism-prereg.md §3): identical
|
||||
# to bridge-federated EXCEPT the willing bridge is drawn from a FINITE
|
||||
# shared pool of size B under a fixed Zipf willingness (skew alpha), so
|
||||
# circuits genuinely REUSE bridges and top-3 concentration varies. The
|
||||
# existing bridge-federated branch above is left untouched (lead cells
|
||||
# stay bit-reproducible).
|
||||
pool_b = int(cell.factors["pool_B"])
|
||||
pool_alpha = float(cell.factors["pool_alpha"])
|
||||
a = _house_nodes(seed, house_size, 1, HOUSES[0], salt=0)[0]
|
||||
b = _house_nodes(seed, house_size, 1, HOUSES[1], salt=0xB)[0]
|
||||
weights = zipf_weights(pool_b, pool_alpha)
|
||||
digest = hashlib.sha256(f"sor-bridge-pool|{seed}".encode()).hexdigest()
|
||||
idx = weighted_draw(digest, weights)
|
||||
hop_specs = [
|
||||
HopSpec(0, "entry", HOUSES[0], a, False),
|
||||
HopSpec(1, "bridge", "bridge", f"bridge#{idx:02d}", True),
|
||||
HopSpec(2, "exit", HOUSES[1], b, False),
|
||||
]
|
||||
houses = (HOUSES[0], HOUSES[1])
|
||||
bridge_present = True
|
||||
elif topo == "directory-federated":
|
||||
directory = _build_directory(seed, house_size, n_houses)
|
||||
path = directory.select_federated_path(seed, hops=hops, min_houses=2)
|
||||
@@ -217,7 +268,7 @@ def assemble(
|
||||
matched_n=matched_n, houses=houses, hops=tuple(hop_specs),
|
||||
)
|
||||
# Federated cells must genuinely span ≥ 2 houses (split-knowledge, RQ2).
|
||||
if topo in ("bridge-federated", "directory-federated") and spec.span_houses() < 2:
|
||||
if topo in ("bridge-federated", "bridge-federated-pool", "directory-federated") and spec.span_houses() < 2:
|
||||
raise ValueError(f"assembler: {cell.cell_id} failed the >=2-house span")
|
||||
return spec
|
||||
|
||||
|
||||
@@ -92,6 +92,40 @@ def enumerate_cells() -> List[Cell]:
|
||||
return cells
|
||||
|
||||
|
||||
def enumerate_rq2p3_cells() -> List[Cell]:
|
||||
"""The RQ2-P3 funnelling-mechanism sweep cells (`rq2p3-mechanism-prereg.md` §4),
|
||||
kept **separate** from the frozen lead lattice: `enumerate_cells()` above is left
|
||||
byte-identical so every lead-pipeline caller (`plan_runs`, `battery_schedule`,
|
||||
`assembler_dry_check`, `collect_rq2_p1_arms`) stays bit-reproducible.
|
||||
|
||||
These use the new `bridge-federated-pool` topology (finite willing-bridge pool of
|
||||
size ``B`` under a Zipf willingness skew ``alpha``) so concentration genuinely
|
||||
varies. Tagged ``rq = "RQ2P3"`` so they are distinct from the frozen RQ1/RQ2
|
||||
cells and are skipped by the lead RQ2 collectors (which filter ``rq == "RQ2"``).
|
||||
|
||||
9 sweep cells: ``B ∈ {2,4,8} × alpha ∈ {0, 1.0, 2.0}``, plus the ``B=50, alpha=0``
|
||||
**calibration anchor** that reproduces the lead RQ2-P3 degeneracy (§7 gate item 1).
|
||||
"""
|
||||
cells: List[Cell] = []
|
||||
for b in (2, 4, 8):
|
||||
for alpha in (0.0, 1.0, 2.0):
|
||||
cells.append(Cell(
|
||||
"RQ2P3",
|
||||
f"RQ2P3/topo=bridge-federated-pool/selector=static/B={b}/alpha={alpha}",
|
||||
{"bridge": "off", "topology": "bridge-federated-pool",
|
||||
"selector": "static", "pool_B": str(b), "pool_alpha": str(alpha)},
|
||||
False,
|
||||
))
|
||||
cells.append(Cell(
|
||||
"RQ2P3",
|
||||
"RQ2P3/topo=bridge-federated-pool/selector=static/B=50/alpha=0.0",
|
||||
{"bridge": "off", "topology": "bridge-federated-pool",
|
||||
"selector": "static", "pool_B": "50", "pool_alpha": "0.0"},
|
||||
False,
|
||||
))
|
||||
return cells
|
||||
|
||||
|
||||
def declared_na_cells() -> List[Cell]:
|
||||
"""N/A cells declared at the design (not run, not dropped ad hoc) — padding is
|
||||
only defined for bridge-on, so bridge-off+padding is N/A (§2)."""
|
||||
|
||||
Reference in New Issue
Block a user