"""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)