295 lines
16 KiB
Python
295 lines
16 KiB
Python
"""Pure pre-outcome DRAFT snapshot policy; no I/O, inputs, execution or RNG.
|
|
|
|
The caller must supply the complete existing trial_plan payload. This module
|
|
does not select a master seed. Its fixed policy choices are newly proposed
|
|
planning choices, not inherited biological measurements or scientific gates.
|
|
|
|
build_evaluation_policy(trial_payload) returns a config directly consumable by
|
|
evaluation_plan.build_evaluation_plan, plus a separate ordered prefix audit.
|
|
Every snapshot boundary is AFTER the complete selected epoch and its scheduled
|
|
delta update, not immediately after the US tick. All returned timing and counts
|
|
describe planned exposures, never observed or accepted DAN spikes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
import hashlib
|
|
import json
|
|
|
|
import evaluation_plan
|
|
import trial_plan
|
|
|
|
|
|
SCHEMA = "phase4an-evaluation-policy-draft-v1"
|
|
ALGORITHM = "fixed-ordered-CS-prefix-completed-epoch-v1"
|
|
POINTS = (0, 1, 2, 5, 10, 20, 50)
|
|
CS_FIELDS = frozenset(("cue", "odor", "occurrence", "stream_id", "source_stream_id",
|
|
"seed", "seed_bits", "seed_conversion", "role", "start_tick", "stop_tick_exclusive"))
|
|
IDENTITY_FIELDS = tuple(sorted(CS_FIELDS - {"stream_id"}))
|
|
SAME_POSITION_ARMS = frozenset(("d_zero", "frozen", "random_reward", "cs_only"))
|
|
|
|
|
|
def _require(condition, message):
|
|
if not condition:
|
|
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()
|
|
|
|
|
|
def _registry_index(trial):
|
|
manifest = trial["seed_manifest"]
|
|
independent = {(r["split"], tuple(r["key"])): r for r in manifest["independent_streams"]}
|
|
aliases = {(r["split"], tuple(r["key"])): r for r in manifest["aliases"]}
|
|
_require(len(independent) == len(manifest["independent_streams"]) and
|
|
len(aliases) == len(manifest["aliases"]) and not (independent.keys() & aliases.keys()),
|
|
"Duplicate or overlapping seed registry keys")
|
|
return independent, aliases
|
|
|
|
|
|
def _stream_id(master, key):
|
|
return "training:" + _sha([trial_plan.SCHEMA, "stream-id", master, "training", list(key)])
|
|
|
|
|
|
def _cs_identity(cs, *, block_id, assay, arm, base_arm, master, registry):
|
|
"""Verify actual alias source/seed; omit only the control's own alias ID."""
|
|
_require(type(cs) is dict and set(cs) == CS_FIELDS, "CS binding schema differs")
|
|
_require(cs["cue"] in trial_plan.CUES and cs["odor"] == trial_plan.ODORS[cs["cue"]] and
|
|
type(cs["occurrence"]) is int and 0 <= cs["occurrence"] < 50 and
|
|
type(cs["seed"]) is int and 0 <= cs["seed"] < 2**63 and
|
|
type(cs["seed_bits"]) is int and cs["seed_bits"] == 63 and cs["role"] == "CS" and
|
|
type(cs["start_tick"]) is int and cs["start_tick"] == 0 and
|
|
type(cs["stop_tick_exclusive"]) is int and cs["stop_tick_exclusive"] == 1000 and
|
|
cs["seed_conversion"] == "exact integer; no truncation or addition",
|
|
"CS cue/occurrence/full seed/role/timing differs")
|
|
key = (block_id, assay, "CS", cs["cue"], f'{cs["occurrence"]:02d}', arm)
|
|
source_key = key[:-1] + (base_arm,)
|
|
independent, aliases = registry
|
|
source = independent.get(("training", source_key))
|
|
_require(source is not None and source["seed"] == cs["seed"] and
|
|
cs["source_stream_id"] == _stream_id(master, source_key) and
|
|
cs["stream_id"] == _stream_id(master, key), "CS seed/source stream authority differs")
|
|
if arm == base_arm:
|
|
_require(("training", key) not in aliases and cs["stream_id"] == cs["source_stream_id"],
|
|
"Original CS must be its own independent source")
|
|
else:
|
|
alias = aliases.get(("training", key))
|
|
_require(alias is not None and alias["source_split"] == "training" and
|
|
alias["source_key"] == list(source_key) and alias["seed"] == cs["seed"] and
|
|
alias["independent_replicate"] is False,
|
|
"CS alias is not registered to this block/assay/cue/occurrence source")
|
|
return {key: cs[key] for key in IDENTITY_FIELDS}
|
|
|
|
|
|
def _prefixes(arm, *, block_id, assay, base_arm, master, registry):
|
|
"""Return verified ordered CS records indexed by actual completed epochs."""
|
|
identities = []
|
|
prefixes = [[]]
|
|
for epoch in arm["epochs"]:
|
|
if epoch["cs"] is not None:
|
|
identities.append(_cs_identity(epoch["cs"], block_id=block_id, assay=assay,
|
|
arm=arm["arm"], base_arm=base_arm, master=master, registry=registry))
|
|
prefixes.append(list(identities))
|
|
if arm["arm"] == "dan_only":
|
|
_require(not identities, "DAN-only must contain no delivered CS")
|
|
else:
|
|
_require(len(identities) == 100, "Every CS-present arm must retain all 100 CS epochs")
|
|
counters = Counter()
|
|
for row in identities:
|
|
_require(row["occurrence"] == counters[row["cue"]], "CS occurrence order is not canonical")
|
|
counters[row["cue"]] += 1
|
|
_require(dict(counters) == {"A": 50, "B": 50}, "Both original odors require 50 CS occurrences")
|
|
return prefixes
|
|
|
|
|
|
def _reinforcement_boundary(arm, point):
|
|
"""The whole epoch containing the kth requested US; terminal includes all."""
|
|
if point == 0:
|
|
return 0
|
|
epochs = arm["epochs"]
|
|
us_indices = [i for i, e in enumerate(epochs) if e["us"]["requested"]]
|
|
_require(len(us_indices) == 50, "Reference arm must have exactly 50 requested US epochs")
|
|
return len(epochs) if point == 50 else us_indices[point - 1] + 1
|
|
|
|
|
|
def _exposure(arm, count, cs_prefix):
|
|
epochs = arm["epochs"][:count]
|
|
return dict(completed_epochs=count, CS_epochs=len(cs_prefix),
|
|
CS_counts_by_cue={cue: sum(c["cue"] == cue for c in cs_prefix) for cue in trial_plan.CUES},
|
|
ordered_CS_source_identity_sha256=_sha(cs_prefix),
|
|
requested_US_epochs=sum(e["us"]["requested"] for e in epochs),
|
|
enabled_nonzero_D_update_opportunities=sum(e["update_enabled"] and e["D_command"] != 0 for e in epochs),
|
|
elapsed_training_ticks=sum(e["steps"] for e in epochs),
|
|
actual_accepted_US_or_DAN_spikes_known=False)
|
|
|
|
|
|
def _suite_policy(block, suite, master, registry):
|
|
"""Independent prefix/accounting construction, separate from evaluation_plan."""
|
|
assay, block_id = suite["assay"], block["block_id"]
|
|
_require(assay in ("normal", "delayed", "reversal"), "Unexpected assay")
|
|
expected_arms = trial_plan.REVERSAL_ARMS if assay == "reversal" else trial_plan.ARMS
|
|
_require([a["arm"] for a in suite["arms"]] == list(expected_arms), "Missing, reordered or duplicate control arms")
|
|
arms = {a["arm"]: a for a in suite["arms"]}
|
|
base_name = "reversal" if assay == "reversal" else "paired"
|
|
base = arms[base_name]
|
|
_require(len(base["epochs"]) == 100, "Base arm must contain all 100 epochs")
|
|
prefixes = {name: _prefixes(arm, block_id=block_id, assay=assay, base_arm=base_name,
|
|
master=master, registry=registry) for name, arm in arms.items()}
|
|
base_full = prefixes[base_name][-1]
|
|
for name, arm in arms.items():
|
|
_require(len(arm["epochs"]) == (150 if name == "unpaired" else 100), "Unexpected epoch coverage")
|
|
if name == "dan_only":
|
|
for i, epoch in enumerate(arm["epochs"]):
|
|
original = base["epochs"][i]
|
|
_require(epoch["cs"] is None and
|
|
epoch["yoked_cue_removed"] == original["cs"]["cue"] and
|
|
type(epoch["yoked_occurrence_removed"]) is int and
|
|
epoch["yoked_occurrence_removed"] == original["cs"]["occurrence"] and
|
|
_canonical(epoch["us"]) == _canonical(original["us"]) and
|
|
epoch["steps"] == original["steps"],
|
|
"DAN-only omitted CS/yoked US/epoch position differs from paired")
|
|
else:
|
|
_require(_canonical(prefixes[name][-1]) == _canonical(base_full),
|
|
"Actual ordered CS cue/occurrence/seed/source sequence differs")
|
|
if name != "unpaired":
|
|
_require(all(_canonical(a) == _canonical(b)
|
|
for a, b in zip(prefixes[name], prefixes[base_name])),
|
|
"CS sequence differs at a paired epoch position")
|
|
|
|
bindings, audits = [], []
|
|
for name in expected_arms:
|
|
arm = arms[name]
|
|
previous = -1
|
|
for point in POINTS:
|
|
base_count = _reinforcement_boundary(base, point)
|
|
expected_prefix = prefixes[base_name][base_count]
|
|
if assay == "reversal":
|
|
count = _reinforcement_boundary(arm, point)
|
|
alignment = "own reinforced-US ordinal; intermediate CS prefixes need not match across arms"
|
|
elif name == "unpaired":
|
|
if point == 0:
|
|
count = 0
|
|
elif point == 50:
|
|
count = len(arm["epochs"])
|
|
else:
|
|
matches = [i for i, prefix in enumerate(prefixes[name]) if len(prefix) == len(expected_prefix)]
|
|
_require(bool(matches), "Unpaired arm never reaches the required CS prefix")
|
|
count = matches[0]
|
|
alignment = "earliest ordered-CS-prefix boundary; terminal includes all 150 epochs"
|
|
else:
|
|
count = base_count
|
|
alignment = ("paired yoked epoch positions; zero delivered CS, not equal CS exposure"
|
|
if name == "dan_only" else "exact paired completed-epoch and ordered-CS prefix")
|
|
_require(count > previous, "Policy snapshot epochs must advance strictly")
|
|
previous = count
|
|
own_prefix = prefixes[name][count]
|
|
same_cs = _canonical(own_prefix) == _canonical(expected_prefix)
|
|
if assay != "reversal" and name != "dan_only":
|
|
_require(same_cs, "Ordered CS prefix mismatch at a control checkpoint")
|
|
if name == "unpaired" and point not in (0, 50):
|
|
_require(count > 0 and len(prefixes[name][count - 1]) < len(expected_prefix),
|
|
"Unpaired snapshot is not the earliest matching completed-epoch boundary")
|
|
actual = _exposure(arm, count, own_prefix)
|
|
reference = _exposure(base, base_count, expected_prefix)
|
|
binding = dict(block_id=block_id, assay=assay, arm=name,
|
|
checkpoint=point, completed_epochs=count)
|
|
bindings.append(binding)
|
|
audits.append(dict(binding=binding, alignment=alignment,
|
|
reference_arm=base_name, reference_completed_epochs=base_count,
|
|
boundary="after complete epoch and scheduled delta update; not at the US tick",
|
|
terminal_includes_all_epochs=point == 50,
|
|
actual=actual, reference=reference,
|
|
ordered_CS_prefix_exact=same_cs if name != "dan_only" else None,
|
|
CS_exposure_equal=same_cs if name != "dan_only" else False,
|
|
DAN_only_yoke_verified=name == "dan_only",
|
|
requested_US_count_equal_to_reference=actual["requested_US_epochs"] == reference["requested_US_epochs"],
|
|
requested_US_count_difference=actual["requested_US_epochs"] - reference["requested_US_epochs"],
|
|
elapsed_ticks_equal_to_reference=actual["elapsed_training_ticks"] == reference["elapsed_training_ticks"],
|
|
elapsed_ticks_difference=actual["elapsed_training_ticks"] - reference["elapsed_training_ticks"],
|
|
nonzero_D_update_opportunity_difference=actual["enabled_nonzero_D_update_opportunities"] - reference["enabled_nonzero_D_update_opportunities"],
|
|
all_US_timing_or_total_exposure_matched_claimed=False))
|
|
return bindings, audits
|
|
|
|
|
|
def _assemble(trial):
|
|
registry = _registry_index(trial)
|
|
bindings, audits = [], []
|
|
blocks = trial["plan"]["blocks"]
|
|
_require(len(blocks) == 40, "Exactly 40 original training blocks are required")
|
|
for block in blocks:
|
|
_require(block["evaluation_checkpoints"] == list(POINTS) and
|
|
[s["assay"] for s in block["suites"]] == ["normal", "delayed", "reversal"],
|
|
"Original assay/checkpoint coverage differs")
|
|
for suite in block["suites"]:
|
|
suite_bindings, suite_audits = _suite_policy(block, suite, trial["plan"]["master"], registry)
|
|
bindings.extend(suite_bindings)
|
|
audits.extend(suite_audits)
|
|
config = dict(schema=evaluation_plan.CONFIG_SCHEMA, probe_steps=1200,
|
|
cs_start_tick=0, cs_stop_tick_exclusive=1000, reversal_checkpoints=list(POINTS),
|
|
baseline_execution="execute_each_snapshot", snapshot_binding_policy="explicit_completed_epochs",
|
|
snapshot_bindings=bindings)
|
|
# Check the unchanged consumer's declared schema/coverage as well as our
|
|
# independent ordered-prefix construction. Do not generate its probes here.
|
|
evaluation_plan._configuration(config)
|
|
evaluation_plan._binding_inventory(trial["plan"], config)
|
|
_require(len(bindings) == 4480 and len({_canonical(v) for v in bindings}) == 4480,
|
|
"Incomplete or duplicate snapshot coverage")
|
|
counts = Counter(row["assay"] for row in bindings)
|
|
result = dict(schema=SCHEMA, status="DRAFT-NOT-REGISTERED", algorithm=ALGORITHM,
|
|
trial_payload_sha256=trial["payload_sha256"],
|
|
seed_manifest_sha256=trial["seed_manifest"]["payload_sha256"],
|
|
config_sha256=_sha(config), config=config,
|
|
audit=dict(bindings=audits,
|
|
coverage=dict(training_blocks=40, training_replicates_per_cell=5,
|
|
nonretention_snapshots=4480, snapshots_by_assay=dict(sorted(counts.items())),
|
|
original_full_CS_alias_sequences_verified=True,
|
|
new_random_streams=0, new_master_seed_selected=False),
|
|
exposure_limits=[
|
|
"CS-present normal/delayed controls match actual ordered CS prefixes, not just counts.",
|
|
"US counts, nonzero-D opportunities and elapsed time are separately reported, not assumed matched.",
|
|
"Unpaired boundaries include earlier inserted US; terminal includes all 150 epochs and trailing US.",
|
|
"DAN-only has zero delivered CS; yoked positions are not equal CS exposure.",
|
|
"Reversal and continuation use their own kth US; intermediate CS prefixes can differ.",
|
|
"All snapshots are after a full epoch plus its delta update, not the US tick.",
|
|
"Requested US is not accepted DAN activity, dopamine concentration or learning evidence."],
|
|
unchanged_retention_scope=dict(arms=["normal/paired", "normal/frozen"],
|
|
idle_checkpoints_seconds=[0, 1, 10],
|
|
same_continuous_ten_second_lineage=True, probes_fork_and_do_not_advance_idle=True,
|
|
retained_snapshot_execution_or_lineage_verified=False),
|
|
conditional_evaluation_inventory=dict(nonretention_probes=8960,
|
|
retention_snapshots=240, retention_probes=480, all_snapshots=4720,
|
|
all_probes=9440, probe_steps=1200, evaluation_steps=11328000,
|
|
includes_full_seven_point_reversal=True,
|
|
baseline_output_deduplication=False,
|
|
actual_execution_count=0)),
|
|
policy_structural_checks_complete=True, choices_fixed_before_outcomes=True,
|
|
choices_are_new_draft_proposals_not_inherited_gates=True,
|
|
protocol_approval_claimed=False, execution_authorized=False, training_executed=False,
|
|
inputs_generated=False, input_generator_implementation_verified=False,
|
|
source_and_snapshot_authority_verified=False, candidate_generated=False,
|
|
learning_pass_claimed=False, scientific_gate_added=False)
|
|
result["payload_sha256"] = _sha(result)
|
|
return json.loads(_canonical(result))
|
|
|
|
|
|
def build_evaluation_policy(trial_payload):
|
|
"""Return fixed DRAFT config/audit; caller provides, we never choose, master."""
|
|
trial_plan.validate_trial_plan(trial_payload)
|
|
return _assemble(trial_payload)
|
|
|
|
|
|
def validate_evaluation_policy(payload, trial_payload):
|
|
"""Exact reconstruction, including JSON types; a replaced self-hash is not authority."""
|
|
_require(type(payload) is dict and "payload_sha256" in payload, "Invalid policy payload")
|
|
_require(payload["payload_sha256"] == _sha({k: v for k, v in payload.items() if k != "payload_sha256"}),
|
|
"Actual evaluation policy SHA mismatch")
|
|
expected = build_evaluation_policy(trial_payload)
|
|
_require(_canonical(payload) == _canonical(expected), "Policy config/audit/coverage differs from exact reconstruction")
|
|
return True
|