Seal AO interface failure and verified isolated postprocessing diagnosis
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
"""Pure, unregistered conditioning inventory; no files, brain inputs or runs.
|
||||
|
||||
build_trial_plan(master) returns plan/seed_manifest/budget. validate_trial_plan
|
||||
checks seed authority, alias permissions, and exact deterministic reconstruction
|
||||
of the complete inventory, not just totals. A consumer must still bind the exact
|
||||
63-bit seed and generator to its generated input receipt; this module cannot
|
||||
certify a future consumer that silently truncates an integer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import seed_registry
|
||||
|
||||
|
||||
SCHEMA = "phase4an-trial-plan-v1-draft"
|
||||
SHUFFLE_VERSION = "sha256-counter-rejection-fisher-yates-v1"
|
||||
MODES = ("physiological", "matched_energy")
|
||||
DAN_BRANCHES = (("PAM15", "y5B'2a"), ("PPL103", "y2a'1"))
|
||||
CUES = ("A", "B")
|
||||
ODORS = {"A": "4-ethylphenol", "B": "3-hexanone"}
|
||||
ARMS = ("paired", "d_zero", "frozen", "random_reward", "unpaired", "cs_only", "dan_only")
|
||||
REVERSAL_ARMS = ("reversal", "no_reversal")
|
||||
CONTROL_ALIASES = frozenset(("d_zero", "frozen", "random_reward", "unpaired", "cs_only"))
|
||||
PROBE_CHECKPOINTS = (0, 1, 2, 5, 10, 20, 50)
|
||||
|
||||
|
||||
def _require(value, message):
|
||||
if not value:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def _canonical(value):
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"),
|
||||
ensure_ascii=False, allow_nan=False)
|
||||
|
||||
|
||||
def _sha(value):
|
||||
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class _Shuffle:
|
||||
"""Explicit portable integer algorithm, unrelated to neural input RNG.
|
||||
|
||||
Each draw hashes canonical UTF-8 JSON [version, full63bitseed, counter].
|
||||
Interpret all 256 digest bits as a big-endian unsigned integer; increment
|
||||
counter even for a rejection. Reject x >= 2**256 - (2**256 % bound), then
|
||||
return x % bound. Fisher-Yates descends i=n-1..1 and swaps with draw(i+1).
|
||||
No Python random/NumPy algorithm, modulo bias, or seed search is used.
|
||||
"""
|
||||
def __init__(self, seed):
|
||||
_require(type(seed) is int and 0 <= seed < 2**63, "Full 63-bit shuffle seed required")
|
||||
self.seed = seed
|
||||
self.counter = 0
|
||||
|
||||
def below(self, bound):
|
||||
_require(type(bound) is int and 0 < bound <= 2**256, "Invalid shuffle bound")
|
||||
limit = 2**256 - (2**256 % bound)
|
||||
while True:
|
||||
raw = bytes.fromhex(_sha([SHUFFLE_VERSION, self.seed, self.counter]))
|
||||
self.counter += 1
|
||||
value = int.from_bytes(raw, "big")
|
||||
if value < limit:
|
||||
return value % bound
|
||||
|
||||
def shuffled(self, values):
|
||||
result = list(values)
|
||||
for i in range(len(result) - 1, 0, -1):
|
||||
j = self.below(i + 1)
|
||||
result[i], result[j] = result[j], result[i]
|
||||
return result
|
||||
|
||||
|
||||
def _stream_id(master, split, key):
|
||||
return split + ":" + _sha([SCHEMA, "stream-id", master, split, list(key)])
|
||||
|
||||
|
||||
def _block_id(mode, dan, reinforced, replicate):
|
||||
return f"{mode}__{dan}__reinforce_{reinforced}__rep{replicate:02d}"
|
||||
|
||||
|
||||
def _input_key(block, assay, cue, occurrence, arm):
|
||||
return (block, assay, "CS", cue, f"{occurrence:02d}", arm)
|
||||
|
||||
|
||||
def _cs_binding(registry, block, assay, cue, occurrence, arm, base_arm):
|
||||
key = _input_key(block, assay, cue, occurrence, arm)
|
||||
source_key = _input_key(block, assay, cue, occurrence, base_arm)
|
||||
if arm == base_arm:
|
||||
seed = registry.add("training", key)
|
||||
else:
|
||||
seed = registry.alias("training", key, "training", source_key,
|
||||
reason="Same block/assay/CS cue/occurrence; named counterfactual control")
|
||||
return {"cue": cue, "odor": ODORS[cue], "occurrence": occurrence,
|
||||
"stream_id": _stream_id(registry.master, "training", key),
|
||||
"source_stream_id": _stream_id(registry.master, "training", source_key),
|
||||
"seed": seed, "seed_bits": 63, "seed_conversion": "exact integer; no truncation or addition",
|
||||
"role": "CS", "start_tick": 0, "stop_tick_exclusive": 1000}
|
||||
|
||||
|
||||
def _ordered_cues(seed):
|
||||
counts = {"A": 0, "B": 0}
|
||||
result = []
|
||||
for cue in _Shuffle(seed).shuffled(["A"] * 50 + ["B"] * 50):
|
||||
result.append((cue, counts[cue]))
|
||||
counts[cue] += 1
|
||||
return result
|
||||
|
||||
|
||||
def _epoch(registry, block, assay, arm, ordinal, cue, occurrence, base_arm,
|
||||
us_ordinal, yoked_cue=None, yoked_occurrence=None):
|
||||
delayed = assay == "delayed"
|
||||
steps, us_tick = (1800, 1500) if delayed else (1200, 900)
|
||||
requested = us_ordinal is not None
|
||||
cs = None if cue is None else _cs_binding(registry, block, assay, cue, occurrence, arm, base_arm)
|
||||
return {
|
||||
"epoch_id": f"{block}/{assay}/{arm}/{ordinal:03d}",
|
||||
"epoch_index": ordinal, "steps": steps, "duration_ms": steps // 10,
|
||||
"initial_neural_state": "frozen_reset_including_KC_trace_and_both_delay_queues",
|
||||
"cs": cs,
|
||||
"us": {"requested": requested,
|
||||
"inventory_id": None if not requested else f"{block}/{assay}/US/{us_ordinal:02d}",
|
||||
"target_binding": None if not requested else block + "/confirmed-DAN-targets",
|
||||
"tick": us_tick if requested else None,
|
||||
"nominal_slot_tick": us_tick,
|
||||
"request_rule": "fixed_single_pulse_all_bound_DAN_targets" if requested else "none",
|
||||
"rng_seed": None, "rng_draws": 0},
|
||||
"D_command": 1.0 if requested and arm != "d_zero" else 0.0,
|
||||
"D_definition": "scheduled artificial scalar, not multiplied by acceptance",
|
||||
"update_enabled": arm != "frozen",
|
||||
"update_schedule": "epoch_boundary_only",
|
||||
"yoked_cue_removed": yoked_cue,
|
||||
"yoked_occurrence_removed": yoked_occurrence,
|
||||
}
|
||||
|
||||
|
||||
def _suite(registry, block, assay, reinforced):
|
||||
base_arm = "reversal" if assay == "reversal" else "paired"
|
||||
order_key = (block, assay, "cue-order")
|
||||
order_seed = registry.add("design", order_key)
|
||||
ordered = _ordered_cues(order_seed)
|
||||
designs = [{"purpose": "cue-order", "stream_id": _stream_id(registry.master, "design", order_key),
|
||||
"seed": order_seed, "algorithm": SHUFFLE_VERSION}]
|
||||
random_sets = None
|
||||
placements = None
|
||||
if assay != "reversal":
|
||||
key = (block, assay, "random-reward-placement")
|
||||
seed = registry.add("design", key)
|
||||
shuffle = _Shuffle(seed)
|
||||
random_sets = {cue: sorted(shuffle.shuffled(range(50))[:25]) for cue in CUES}
|
||||
designs.append({"purpose": "random-reward-placement", "stream_id": _stream_id(registry.master, "design", key),
|
||||
"seed": seed, "algorithm": SHUFFLE_VERSION})
|
||||
key = (block, assay, "unpaired-US-placement")
|
||||
seed = registry.add("design", key)
|
||||
placements = _Shuffle(seed).shuffled(["CS"] * 100 + ["US"] * 50)
|
||||
designs.append({"purpose": "unpaired-US-placement", "stream_id": _stream_id(registry.master, "design", key),
|
||||
"seed": seed, "algorithm": SHUFFLE_VERSION})
|
||||
|
||||
arms = []
|
||||
for arm in REVERSAL_ARMS if assay == "reversal" else ARMS:
|
||||
events = []
|
||||
arm_reinforced = ("B" if reinforced == "A" else "A") if arm == "reversal" else reinforced
|
||||
if arm == "unpaired":
|
||||
cursor, us_cursor = 0, 0
|
||||
for token in placements:
|
||||
if token == "CS":
|
||||
cue, occurrence = ordered[cursor]
|
||||
cursor += 1
|
||||
events.append(_epoch(registry, block, assay, arm, len(events), cue, occurrence, base_arm, None))
|
||||
else:
|
||||
events.append(_epoch(registry, block, assay, arm, len(events), None, None, base_arm, us_cursor))
|
||||
us_cursor += 1
|
||||
else:
|
||||
reward_us_ids = None
|
||||
if arm == "random_reward":
|
||||
reward_us_ids = {(cue, occurrence): offset + i
|
||||
for cue, offset in (("A", 0), ("B", 25))
|
||||
for i, occurrence in enumerate(random_sets[cue])}
|
||||
for cue, occurrence in ordered:
|
||||
if arm == "random_reward":
|
||||
us_ordinal = reward_us_ids.get((cue, occurrence))
|
||||
elif arm == "cs_only":
|
||||
us_ordinal = None
|
||||
else:
|
||||
us_ordinal = occurrence if cue == arm_reinforced else None
|
||||
if arm == "dan_only":
|
||||
events.append(_epoch(registry, block, assay, arm, len(events), None, None, base_arm,
|
||||
us_ordinal, yoked_cue=cue, yoked_occurrence=occurrence))
|
||||
else:
|
||||
events.append(_epoch(registry, block, assay, arm, len(events), cue, occurrence, base_arm, us_ordinal))
|
||||
arms.append({
|
||||
"arm": arm,
|
||||
"delta_start": {"kind": "fork", "source": f"{block}/normal/paired/terminal-delta"}
|
||||
if assay == "reversal" else {"kind": "zero", "source": None},
|
||||
"carry_between_epochs": ["plastic_delta_only"],
|
||||
"reinforced_cue": arm_reinforced if arm not in ("random_reward", "unpaired", "cs_only", "dan_only") else None,
|
||||
"random_reward_occurrences": random_sets if arm == "random_reward" else None,
|
||||
"epochs": events,
|
||||
})
|
||||
return {"assay": assay, "design_streams": designs, "arms": arms}
|
||||
|
||||
|
||||
def _assemble(master):
|
||||
registry = seed_registry.SeedRegistry(master)
|
||||
blocks = []
|
||||
for mode in MODES:
|
||||
for dan, compartment in DAN_BRANCHES:
|
||||
for reinforced in CUES:
|
||||
for replicate in range(1, 6):
|
||||
block = _block_id(mode, dan, reinforced, replicate)
|
||||
evaluations = []
|
||||
for cue in CUES:
|
||||
key = _input_key(block, "evaluation", cue, 0, "base-probe")
|
||||
seed = registry.add("evaluation", key)
|
||||
evaluations.append({"cue": cue, "odor": ODORS[cue], "role": "CS", "seed": seed,
|
||||
"seed_bits": 63, "stream_id": _stream_id(master, "evaluation", key),
|
||||
"use": "same saved input across pre/post/controls; never extra training replicate"})
|
||||
blocks.append({
|
||||
"block_id": block, "mode": mode, "dan_type": dan,
|
||||
"compartment_verbatim": compartment, "replicate": replicate,
|
||||
"initial_reinforced_cue": reinforced,
|
||||
"initial_neutral_cue": "B" if reinforced == "A" else "A",
|
||||
"DAN_target_binding": {"binding_id": block + "/confirmed-DAN-targets",
|
||||
"source": "sealed H exact confirmed mapping; not materialized by this planner",
|
||||
"required_before_execution": True, "body_ids": None},
|
||||
"suites": [_suite(registry, block, assay, reinforced)
|
||||
for assay in ("normal", "delayed", "reversal")],
|
||||
"evaluation_streams": evaluations,
|
||||
"evaluation_checkpoints": list(PROBE_CHECKPOINTS),
|
||||
"evaluation_policy": "separate future probe inventory required; evaluation exposure not included in epoch budget",
|
||||
"retention": {"branches": ["normal/paired", "normal/frozen"],
|
||||
"checkpoint_seconds": [0, 1, 10], "idle_seconds_each": 10,
|
||||
"state_continuous": True, "probes_fork_and_do_not_advance_idle": True},
|
||||
})
|
||||
|
||||
plan = {
|
||||
"schema": SCHEMA, "status": "DRAFT-NOT-REGISTERED", "master": master,
|
||||
"execution_authorized": False, "brain_inputs_generated": False,
|
||||
"training_executed": False, "shuffle_algorithm": SHUFFLE_VERSION,
|
||||
"shuffle_specification": _Shuffle.__doc__.strip(),
|
||||
"input_seed_contract": "full63bit integer; no uint32 truncation, offset, modulo or hidden global RNG",
|
||||
"consumer_generator_implementation_verified": False,
|
||||
"fixed_odors": dict(ODORS), "dt_ms": 0.1,
|
||||
"training_replicates_per_cell": 5, "blocks": blocks,
|
||||
"alias_policy": "training CS only; same block/assay/cue/occurrence; named controls only",
|
||||
"scheduled_US_rng": "none; deterministic pulse; design shuffle randomness is separately registered",
|
||||
}
|
||||
counts, steps = Counter(), 0
|
||||
cs_epochs, us_epochs, effective_updates = 0, 0, 0
|
||||
for block in blocks:
|
||||
for suite in block["suites"]:
|
||||
for arm in suite["arms"]:
|
||||
counts[(suite["assay"], arm["arm"])] += len(arm["epochs"])
|
||||
for epoch in arm["epochs"]:
|
||||
steps += epoch["steps"]
|
||||
cs_epochs += epoch["cs"] is not None
|
||||
us_epochs += epoch["us"]["requested"]
|
||||
effective_updates += epoch["update_enabled"] and epoch["D_command"] != 0
|
||||
budget = {
|
||||
"blocks": 40, "training_replicates_per_cell": 5,
|
||||
"epoch_counts_by_assay_arm": [{"assay": a, "arm": b, "epochs": count}
|
||||
for (a, b), count in sorted(counts.items())],
|
||||
"training_control_reversal_epochs": sum(counts.values()),
|
||||
"CS_epochs": cs_epochs, "US_request_epochs": us_epochs,
|
||||
"enabled_nonzero_D_update_opportunities": effective_updates,
|
||||
"training_control_reversal_steps": steps,
|
||||
"training_control_reversal_biological_seconds": steps // 10000,
|
||||
"retention_idle_seconds": 40 * 2 * 10,
|
||||
"retention_idle_steps": 40 * 2 * 10 * 10000,
|
||||
"minimum_biological_seconds_excluding_evaluation": steps // 10000 + 800,
|
||||
"minimum_steps_excluding_evaluation": steps + 8000000,
|
||||
"evaluation_epochs": None, "evaluation_biological_seconds": None,
|
||||
"evaluation_probe_inventory_complete": False,
|
||||
"excludes": ["evaluation probes", "implementation validation", "frozen regressions", "I/O and encoding wall time"],
|
||||
"reason_not_total_runtime": "Only training/control/reversal and continuous retention idle are budgeted; never label this complete total exposure",
|
||||
}
|
||||
_require(budget["training_control_reversal_epochs"] == 68000 and steps == 99600000,
|
||||
"Internal protocol inventory arithmetic changed")
|
||||
result = {"plan": plan, "seed_manifest": registry.manifest(), "budget": budget}
|
||||
result["payload_sha256"] = _sha(result)
|
||||
return result
|
||||
|
||||
|
||||
def _validate_alias_permissions(manifest):
|
||||
for row in manifest["aliases"]:
|
||||
key, source = row["key"], row["source_key"]
|
||||
_require(row["split"] == row["source_split"] == "training",
|
||||
"Only named training CS controls may alias streams")
|
||||
_require(len(key) == len(source) == 6 and key[2] == source[2] == "CS",
|
||||
"CS/US/design/evaluation role aliasing is forbidden")
|
||||
_require(key[:5] == source[:5], "Alias crosses block/replicate/assay/cue/occurrence")
|
||||
if key[1] in ("normal", "delayed"):
|
||||
_require(source[5] == "paired" and key[5] in CONTROL_ALIASES,
|
||||
"Unknown normal/delayed control alias")
|
||||
else:
|
||||
_require(key[1] == "reversal" and source[5] == "reversal" and key[5] == "no_reversal",
|
||||
"Unknown reversal control alias")
|
||||
|
||||
|
||||
def validate_trial_plan(payload):
|
||||
"""Fail closed on incomplete coverage, illegal aliasing, mutation or types.
|
||||
|
||||
Rebuild the whole fixed 40-block schedule and every RNG derivation. This
|
||||
does not trust a count, a caller's declared sha, or a row labeled replicate.
|
||||
Canonical comparison distinguishes Boolean/integer/floating-point JSON.
|
||||
"""
|
||||
_require(type(payload) is dict and set(payload) == {"plan", "seed_manifest", "budget", "payload_sha256"},
|
||||
"Invalid complete trial-plan envelope")
|
||||
actual = {key: value for key, value in payload.items() if key != "payload_sha256"}
|
||||
_require(type(payload["payload_sha256"]) is str and payload["payload_sha256"] == _sha(actual),
|
||||
"Actual trial-plan payload SHA mismatch")
|
||||
seed_registry.validate_manifest(payload["seed_manifest"])
|
||||
_validate_alias_permissions(payload["seed_manifest"])
|
||||
_require(payload["plan"]["master"] == payload["seed_manifest"]["master"], "Master identities differ")
|
||||
expected = _assemble(payload["plan"]["master"])
|
||||
_require(_canonical(payload) == _canonical(expected),
|
||||
"Full protocol coverage/input binding/order/type reconstruction mismatch")
|
||||
return True
|
||||
|
||||
|
||||
def build_trial_plan(master):
|
||||
"""Return a deterministic JSON-serializable DRAFT inventory without I/O."""
|
||||
return _assemble(master)
|
||||
Reference in New Issue
Block a user