Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b92691
Track B B0.2: plan, landscape, protocol v1 (draft) and count script
MaxGhenis Oct 25, 2026
3fe9541
B0.2: 300 seconds per program, after round 3's withheld rerun
MaxGhenis Oct 25, 2026
f9d6dc4
B0.2: warnings print nothing; an interruption is a failure
MaxGhenis Oct 25, 2026
74ed29b
B0.2: no rounds in any output; provenance limited to shape and input …
MaxGhenis Oct 25, 2026
7f65791
B0.2: a stop withholds the version; the release reports its phase alone
MaxGhenis Oct 25, 2026
60d77cf
B0.2: the run's conditions in place of a wall limit
MaxGhenis Oct 25, 2026
80c5ab7
B0.2: review round 4's test gaps; the evidence folder's exclusions
MaxGhenis Oct 25, 2026
a3b6236
Merge origin/master (51323bed) into trackb-b0-2-v1
MaxGhenis Oct 25, 2026
3747d0e
B0.2: the separation check belongs to the exclusion tests
MaxGhenis Oct 25, 2026
1f3b529
B0.2: round 4's script and block items, and the run's conditions
MaxGhenis Oct 25, 2026
cc144b2
B0.2: tier recount after round 4's items
MaxGhenis Oct 25, 2026
0f94254
B0.2: the QoS rule and the wait's bound as the coordinator set them
MaxGhenis Oct 25, 2026
c6aec97
B0.2: tier recount after the QoS and bounded-wait tests
MaxGhenis Oct 25, 2026
9322a0b
B0.2: an assertion counts as held only at the on level; a wider test …
MaxGhenis Oct 25, 2026
c8d6dbb
Merge master 7e050df5 into trackb-b0-2-v1
MaxGhenis Oct 25, 2026
9b9d53f
B0.2: tier recount for the two assertion-level tests
MaxGhenis Oct 25, 2026
3e169c7
B0.2: round 5's second attempt: the status example, two uncaught muta…
MaxGhenis Oct 25, 2026
05c957a
B0.2: tier recount for the ownership and stratum tests
MaxGhenis Oct 25, 2026
226d844
Merge master 1ec57a5d into trackb-b0-2-v1
MaxGhenis Oct 25, 2026
e79f41b
Merge master 444f970d into trackb-b0-2-v1
MaxGhenis Oct 26, 2026
abbbf79
B0.2: a test that the audit uses the relations it registers
MaxGhenis Oct 26, 2026
8a30da5
B0.2: tests for the record's two other survivors
MaxGhenis Oct 26, 2026
c5e30f4
B0.2: tier recount for the three survivor tests
MaxGhenis Oct 26, 2026
1e3ff4d
Merge master 172f690f into trackb-b0-2-v1
MaxGhenis Oct 26, 2026
10f03e2
B0.2: calibrations with and without B0.1's sets, and the singleton di…
MaxGhenis Oct 26, 2026
6cca06a
B0.2: exempt a target B0.1's published counts already confine as tightly
MaxGhenis Oct 26, 2026
1ed6a26
B0.2: the exemption covers classes and differences alike
MaxGhenis Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,027 changes: 1,027 additions & 0 deletions docs/design/track_b_b0_2_counts_mutations.json

Large diffs are not rendered by default.

262 changes: 262 additions & 0 deletions docs/design/track_b_b0_2_counts_report.txt

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions docs/design/track_b_b0_2_evidence/calibrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Time B0.2's disclosure audit on an invented panel.

INVENTED DATA ONLY. ``invented_panel.py`` builds the panel from a seeded
generator; no PSID file is read. The script builds the count script's
registry and model, runs its audit at the fixed base, and prints the
model's size, the audit's status and results, and its slowest targets.
As ``main()`` does, it registers B0.1's sets, rebuilt from the same panel,
at the counts B0.1's own script gives; ``--without-b0-1`` leaves them out,
as the runs before review round 3's answer did.
It wraps ``classify`` and ``AuditSystem.direction`` only to time them; the
audit itself is unchanged.

Run from the repository root, with the thread settings the protocol uses::

OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 OMP_WAIT_POLICY=PASSIVE \\
PYTHONPATH=src:. python docs/design/track_b_b0_2_evidence/calibrate.py \\
HOUSEHOLDS SEED [--without-b0-1]

The output for the report is committed beside it as ``calibration.txt``.
"""

from __future__ import annotations

import sys
import time
from collections import Counter
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parents[2] / "scripts"))
sys.path.insert(0, str(HERE))

import track_b_b0_2_counts as script # noqa: E402
from invented_panel import invented_inputs # noqa: E402


def main(households: int, seed: int, with_b0_1: bool = True) -> None:
started = time.monotonic()
people, labor, anchor, design = invented_inputs(households, seed)
validity = {
wave: script.validity_frame(frame) for wave, frame in labor.items()
}
counts = script.structural_counts(people, validity, anchor, design)
registry = script.disclosure_registry(people, validity, anchor, design)
earlier = {}
if with_b0_1:
earlier = script.b0_1_sets(people, validity)
script.register_b0_1(
registry, earlier, script.b0_1_structural_counts(people, validity)
)
model = script.build_model(registry)
sensitive = sum(1 <= t["true_size"] < 10 for t in model["targets"])
print(
f"households={households} seed={seed} "
f"persons/wave~{len(people) // 13} sets={len(registry.sets)} "
f"b0_1_sets={len(earlier)} "
f"rows={model['matrix'].shape[0]} "
f"atoms={dict(Counter(model['unit_types']))} "
f"targets={len(model['targets'])} sensitive={sensitive} "
f"build={time.monotonic() - started:.1f}s",
flush=True,
)
slow = []
classify = script.classify
direction = script.AuditSystem.direction

def timed_classify(system, target, pool, *, seconds):
start = time.monotonic()
outcome = classify(system, target, pool, seconds=seconds)
elapsed = time.monotonic() - start
record = outcome[0]
slow.append(
(
round(elapsed, 1),
target["kind"],
target["true_size"],
record["result"],
record.get("proof"),
)
)
return outcome

def timed_direction(self, weights, *, seconds):
start = time.monotonic()
point = direction(self, weights, seconds=seconds)
print(
f" seed program {time.monotonic() - start:.1f}s "
f"point={point is not None}",
flush=True,
)
return point

script.classify = timed_classify
script.AuditSystem.direction = timed_direction
start = time.monotonic()
audit = script.audit_counts(counts, registry, script.DISCLOSURE_BASE)
results = Counter(t["result"] for t in audit["targets"])
proofs = Counter(t.get("proof") for t in audit["targets"])
narrow_sensitive = sum(
t["result"] == "narrow" and 1 <= t["true_size"] < 10
for t in audit["targets"]
)
print(
f"B={audit['base']} status={audit['status']} "
f"rounds={[len(r['disclosed']) for r in audit['rounds']]} "
f"results={dict(results)} proofs={dict(proofs)} "
f"pool={audit['pool']} narrow_sensitive={narrow_sensitive} "
f"audit={time.monotonic() - start:.1f}s",
flush=True,
)
print(" slowest targets:", sorted(slow, reverse=True)[:5], flush=True)
withholding = [
(t["kind"], t["unit_type"], t["true_size"], t["earlier"])
for t in audit["targets"]
if t["result"] == "narrow" and 1 <= t["true_size"] < 10
]
print(" narrow sensitive (kind, unit, size, earlier):", withholding)


if __name__ == "__main__":
main(
int(sys.argv[1]),
int(sys.argv[2]),
with_b0_1="--without-b0-1" not in sys.argv[3:],
)
93 changes: 93 additions & 0 deletions docs/design/track_b_b0_2_evidence/calibration.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
B0.2 disclosure audit on invented panels (INVENTED DATA ONLY; no PSID file read).
Script: docs/design/track_b_b0_2_evidence/calibrate.py (SHA-256 c714f0d8...),
loading scripts/track_b_b0_2_counts.py (7ec6ee28...) and invented_panel.py
(38c17076...), each with the bytes committed at e79f41b5; none has changed since.
Run 2026-10-26 from a clean clone at e79f41b5, one job at a time on a shared host,
each job at nice 10 on one core, with OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1
OMP_WAIT_POLICY=PASSIVE PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src:. The one-minute
load average was 63 to 97 when each job started. Times are wall clock and vary
with host load. Each program's limit is 300 s (PROGRAM_SECONDS).

The first three runs register B0.1's sets at the counts B0.1's own script gives,
as main() does (b0_1_sets=71). The last two leave them out (--without-b0-1), on
the same two panels that withhold with them.

An earlier calibration, at 88211bf8 on 2026-10-24, used a 60 s limit and no B0.1
sets. On the same three panels it released, every target wide: 800 households,
2,843 targets (audit 298.5 s); 3,000 households, 3,112 (699.2 s); 9,000
households, 3,776 (705.2 s).

$ python docs/design/track_b_b0_2_evidence/calibrate.py 800 1
households=800 seed=1 persons/wave~1341 sets=588 b0_1_sets=71 rows=512 atoms={'clusters': 156, 'households_1999': 5, 'households_2001': 6, 'households_2003': 6, 'households_2005': 6, 'households_2007': 61, 'households_2009': 5, 'households_2011': 119, 'households_2013': 10, 'households_2015': 10, 'households_2017': 6, 'households_2019': 5, 'households_2021': 6, 'households_2023': 6, 'persons': 685, 'strata': 80} targets=3081 sensitive=939 build=0.5s
seed program 7.5s point=True
seed program 3.5s point=True
seed program 5.8s point=True
seed program 3.2s point=True
seed program 8.5s point=True
seed program 8.3s point=True
seed program 4.0s point=True
seed program 3.7s point=True
seed program 3.2s point=True
B=10 status=withheld rounds=[0] results={'wide': 366, 'narrow': 1, 'unreached': 2714} proofs={'pool': 327, 'search': 39, 'optima': 1, None: 2714} pool=48 narrow_sensitive=1 audit=246.8s
slowest targets: [(7.7, 'atom', 3, 'wide', 'search'), (7.4, 'atom', 1, 'wide', 'search'), (7.4, 'atom', 1, 'wide', 'search'), (7.3, 'atom', 1, 'wide', 'search'), (7.1, 'atom', 1, 'wide', 'search')]
narrow sensitive (kind, unit, size, earlier): [('atom', 'persons', 1, False)]

$ python docs/design/track_b_b0_2_evidence/calibrate.py 3000 2
households=3000 seed=2 persons/wave~5108 sets=588 b0_1_sets=71 rows=506 atoms={'clusters': 160, 'households_1999': 6, 'households_2001': 6, 'households_2003': 6, 'households_2005': 6, 'households_2007': 88, 'households_2009': 6, 'households_2011': 197, 'households_2013': 10, 'households_2015': 10, 'households_2017': 6, 'households_2019': 6, 'households_2021': 6, 'households_2023': 6, 'persons': 1747, 'strata': 80} targets=3228 sensitive=1446 build=1.1s
seed program 16.5s point=True
seed program 12.7s point=True
seed program 5.6s point=True
seed program 15.1s point=True
seed program 18.8s point=True
seed program 6.6s point=True
seed program 3.6s point=True
seed program 11.3s point=True
seed program 18.4s point=True
B=10 status=withheld rounds=[0] results={'wide': 1251, 'narrow': 1, 'unreached': 1976} proofs={'search': 81, 'pool': 1170, 'optima': 1, None: 1976} pool=90 narrow_sensitive=1 audit=1012.2s
slowest targets: [(25.3, 'atom', 1, 'wide', 'search'), (22.4, 'atom', 3, 'wide', 'search'), (21.8, 'atom', 1, 'wide', 'search'), (20.3, 'atom', 1, 'wide', 'search'), (20.0, 'atom', 1, 'wide', 'search')]
narrow sensitive (kind, unit, size, earlier): [('atom', 'persons', 1, False)]

$ python docs/design/track_b_b0_2_evidence/calibrate.py 9000 3
households=9000 seed=3 persons/wave~15348 sets=588 b0_1_sets=71 rows=482 atoms={'clusters': 160, 'households_1999': 6, 'households_2001': 6, 'households_2003': 6, 'households_2005': 6, 'households_2007': 123, 'households_2009': 6, 'households_2011': 272, 'households_2013': 11, 'households_2015': 11, 'households_2017': 6, 'households_2019': 6, 'households_2021': 6, 'households_2023': 6, 'persons': 3795, 'strata': 77} targets=3838 sensitive=2245 build=2.3s
seed program 7.3s point=True
seed program 6.7s point=True
seed program 8.7s point=True
seed program 10.0s point=True
seed program 20.4s point=True
seed program 6.9s point=True
seed program 9.5s point=True
seed program 10.5s point=True
seed program 14.1s point=True
B=10 status=released rounds=[0] results={'wide': 3820, 'irrelevant': 18} proofs={'pool': 3623, 'search': 197, None: 18} pool=206 narrow_sensitive=0 audit=2245.4s
slowest targets: [(31.5, 'atom', 1, 'wide', 'search'), (31.4, 'atom', 1, 'wide', 'search'), (31.0, 'atom', 3, 'wide', 'search'), (30.0, 'atom', 7, 'wide', 'search'), (29.7, 'atom', 1, 'wide', 'search')]
narrow sensitive (kind, unit, size, earlier): []

$ python docs/design/track_b_b0_2_evidence/calibrate.py 800 1 --without-b0-1
households=800 seed=1 persons/wave~1341 sets=588 b0_1_sets=0 rows=462 atoms={'clusters': 156, 'households_1999': 5, 'households_2001': 6, 'households_2003': 6, 'households_2005': 6, 'households_2007': 61, 'households_2009': 5, 'households_2011': 72, 'households_2013': 10, 'households_2015': 10, 'households_2017': 6, 'households_2019': 5, 'households_2021': 6, 'households_2023': 6, 'persons': 471, 'strata': 80} targets=2843 sensitive=939 build=0.4s
seed program 0.8s point=True
seed program 2.8s point=True
seed program 2.5s point=True
seed program 3.0s point=True
seed program 2.9s point=True
seed program 2.6s point=True
seed program 3.8s point=True
seed program 2.5s point=True
seed program 3.0s point=True
B=10 status=released rounds=[0] results={'wide': 2843} proofs={'pool': 2773, 'search': 70} pool=79 narrow_sensitive=0 audit=275.6s
slowest targets: [(8.4, 'atom', 1, 'wide', 'search'), (7.6, 'atom', 1, 'wide', 'search'), (6.8, 'atom', 1, 'wide', 'search'), (6.7, 'atom', 1, 'wide', 'search'), (6.6, 'atom', 1, 'wide', 'search')]
narrow sensitive (kind, unit, size, earlier): []

$ python docs/design/track_b_b0_2_evidence/calibrate.py 3000 2 --without-b0-1
households=3000 seed=2 persons/wave~5108 sets=588 b0_1_sets=0 rows=456 atoms={'clusters': 160, 'households_1999': 6, 'households_2001': 6, 'households_2003': 6, 'households_2005': 6, 'households_2007': 88, 'households_2009': 6, 'households_2011': 115, 'households_2013': 10, 'households_2015': 10, 'households_2017': 6, 'households_2019': 6, 'households_2021': 6, 'households_2023': 6, 'persons': 1050, 'strata': 80} targets=3112 sensitive=1446 build=0.6s
seed program 1.9s point=True
seed program 3.4s point=True
seed program 12.3s point=True
seed program 7.3s point=True
seed program 3.2s point=True
seed program 4.0s point=True
seed program 8.5s point=True
seed program 14.3s point=True
seed program 19.8s point=True
B=10 status=released rounds=[0] results={'wide': 3112} proofs={'search': 84, 'pool': 3028} pool=93 narrow_sensitive=0 audit=667.8s
slowest targets: [(19.8, 'atom', 1, 'wide', 'search'), (14.6, 'atom', 1, 'wide', 'search'), (14.4, 'atom', 2, 'wide', 'search'), (13.0, 'atom', 2, 'wide', 'search'), (12.8, 'atom', 2, 'wide', 'search')]
narrow sensitive (kind, unit, size, earlier): []
62 changes: 62 additions & 0 deletions docs/design/track_b_b0_2_evidence/exhaust_closure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Search every three-atom table for a release that needs a second round.

INVENTED TABLES ONLY. A table here is any set of two or more published
rows over three atoms whose signatures (the rows that hold each atom) are
distinct and nonempty, and every size vector with each atom in 1 to 14.
For each, ``oracle.release`` applies the rule, and the script prints each
table whose release discloses a target in a closure round. It found none,
which is why the closure rounds are tested by a forced-round unit test
instead (``test_a_narrow_target_outside_one_to_nine_starts_another_round``).

Run from the repository root::

python docs/design/track_b_b0_2_evidence/exhaust_closure.py

The output is committed beside it as ``exhaust_closure.txt``.
"""

from __future__ import annotations

import sys
from itertools import combinations, product
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

import oracle # noqa: E402

ATOMS = 3


def main() -> None:
rows_available = [
row
for size in range(1, ATOMS + 1)
for row in combinations(range(ATOMS), size)
]
structures = 0
tables = 0
multi = 0
for count in range(2, len(rows_available) + 1):
for rows in combinations(rows_available, count):
signatures = {
frozenset(j for j, row in enumerate(rows) if a in row)
for a in range(ATOMS)
}
if len(signatures) != ATOMS or frozenset() in signatures:
continue
structures += 1
target_list = oracle.targets(rows, ATOMS)
for sizes in product(range(1, 15), repeat=ATOMS):
tables += 1
status, disclosed, _ = oracle.release(
list(rows), list(sizes), None, target_list
)
if disclosed:
multi += 1
print("MULTI", rows, sizes, status, disclosed)
print(f"structures {structures} tables {tables} multi {multi}")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions docs/design/track_b_b0_2_evidence/exhaust_closure.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
structures 96 tables 263424 multi 0
Loading
Loading