Prepare unregistered conditioning evaluation input and update adapters

This commit is contained in:
Digital Drosophila Archive
2026-09-30 17:09:40 +08:00
parent aeccb7d0cc
commit c187d7d5a6
24 changed files with 4779 additions and 2 deletions
+6 -2
View File
@@ -44,14 +44,18 @@ Phase 4AK D18 表征验证 PASS 原样保留。1.8 ms 为工程 APL 时序假设
Phase 4AN 的有限工程门已封存;21 份实际使用源码已按 SHA 锁定,lab 整个目录未封存。科学学习草案仍未登记,没有候选;工程测试不是条件化实验。
完整 conditioning 草案已有 68,000 epochs 与合计 10,760 生物秒(含 idle),但 evaluation/probe 库存未完整。按当前 100 ms 样本算术外推,仅核心模拟约 64–66 小时,不能直接作为完整工期或预算。先补齐评估/控制/保持集成与有限预算,再登记小规模机制验证,不直接启动大批量训练。
新增科学代码仍为未注册 DRAFT:评估清单、按真实 CS 序列对齐的控制政策、完整 63 位种子的输入接口,以及重用固定 AM/J 实现的单轮 eligibility 更新桥。后者只返回 delta,不安装权重。详见[条件化准备进度与限制](docs/progress/conditioning-preparation-2026-09-30.md)。
服务器 CPU 合成测试 119/119 通过,0 跳过;21 个 AN 注册源测试前后 SHA 均未变。当前续跑器禁止父子段更换 overlay,因此“保留末态并使用新 delta”的 retention 更新边界仍需新增接口验证,不是现有工程 PASS 已覆盖的能力。
完整 conditioning 草案为 68,000 epochs、10,760 生物秒(含 idle),新显式评估政策再列出 9,440 次 probes / 1,132.8 生物秒,总计 11,892.8 生物秒。这是未执行的条件性计划库存,不是工期预算。仍缺实际输入/状态/delta 绑定协调器、真实 CUDA 输入一致性验证、反转/连续保持功能门和完整资源预算;先登记有限实现检查,不直接启动大批量训练。
目标仍是“学习可以使用”,当前没有达到。接口验证或静态权重效果不能替代真实条件化。
后续须预注册固定 olfactory A/B、具体 DAN type/compartment、至少五个互不重叠训练种子流及独立 evaluation streams;保留原 eligibility-v2、人工 US 与实际 DAN 放电的明确区分,以及配对/控制、反转、延迟奖励、保持等功能验证。
继续冻结 raw/base CSR/W_syn/sign、既有 APL 参数、eligibility-v2、14,232-edge mask 和全部已封存产物。不调参挽救结果,不接游戏、视觉、decoder、RL 或 Web。
AM 已正式封存并在服务器发布 3,020 份小证据;本地已按发布收据逐项验收 3,040 个文件(含控制与清单文件)。Git 同步范围仅为源码、文档及小证据,须单独验证提交载荷。大型轨迹保留在服务器,小证据同步不等于全量异机备份。
AM 已正式封存并在服务器发布 3,020 份小证据;本地已按发布收据逐项验收 3,040 个文件(含控制与清单文件)。AN 的 599 个小证据文件已逐项本地验收;620 个实际 Git blobs 校验通过并推送为 `aeccb7d0ccdcf6788e0a8de60e7ab96f70dbe734`。Git 同步范围仅为源码、文档及小证据。大型轨迹保留在服务器,小证据同步不等于全量异机备份。
模型 manifest SHA-256:`7edeb251ecbeac5a64805fefe171a0a0bf8bba2675919ef922cc1a329c203ea8`。
实验 manifest SHA-256:`9e24b682935a9b18e05db7a933080c5f63ddbeb8a0c85cc0df1cfe1d06d88ab4`。
@@ -0,0 +1,56 @@
# 独立条件化分支:准备进度与边界
状态:**未注册的科学实现草案,不是 learning PASS,不是 learned candidate**。
已完成的 Phase 4AN 工程证据另行封存并推送至
`aeccb7d0ccdcf6788e0a8de60e7ab96f70dbe734`:32/32 次 GPU 运行、74 项精确比较通过。
599 个小证据文件已下载到本地逐项验收,共 5,508,335 bytes;620 个实际 Git blobs
(含 21 个注册源文件)核对通过。大体积轨迹仍在服务器,不是全量异机备份。
## 本次补齐的实现
1. 评估清单:完整重建训练计划和独立评估种子来源;同一训练重复内复用 A/B
评估输入作配对比较,不能把反复 probe 当成更多训练重复。
2. 显式评估政策:CS 为 0–100 ms,观察到 120 ms;保留 0/1/2/5/10/20/50
检查点。控制组核对实际有序 CS 前缀及源种子,不只核对数量。
3. 输入接口:保留原离散 float32 Bernoulli 抽样、PN 列顺序和完整 63 位种子;
必须指定 CUDA 设备和抽样上限。CS/US 重叠默认报错,显式布尔合并才允许并记录冲突。
4. 单轮更新桥:复用固定 SHA 的 AM/J 原实现,从本轮真实稀疏事件计算 eligibility,
在完整 epoch 结束后返回下一轮 delta;只计算,不安装权重,不改 base。
这里的输入单元测试使用 CPU/mock,不代表已经实测 CUDA 随机序列或高位种子的作用。
单轮更新测试使用合成事件,不是已经运行条件化。人工 D 指令与实际 DAN 放电仍是两项证据。
attempted update、实际 delta 改变和已达 depression floor 分别统计。
服务器 CPU 合成测试:**119/119 通过,0 跳过**,测试框架记录耗时 241.205 s。
其中包含原 Torch CPU eligibility 表达式与 NumPy 路径的合成数值比较,不是 CUDA 比较。
运行时隐藏全部 CUDA 设备;没有真实脑输入、网络模拟或 conditioning。21 个 AN 注册源在
测试前后均另行校验保持原 SHA。
## 对齐不等于所有条件相同
非配对控制的中间奖励次数、经过时间可以与配对组不同;随机奖励的中间奖励次数也可能不同。
这些差异单独记录,不称为全量匹配。DAN-only 的 CS 被有意移除,不能声称 CS exposure 相同。
反转两臂各按自己的强化机会取点,但保留原始 acquisition 的气味方向与 pre baseline。
所有检查点都在完整 epoch 及其边界更新之后,不是在 US tick 当场插入 probe。
该政策条件性列出 9,440 次评估(1,132.8 生物秒),加已有训练/控制/连续 idle 共
11,892.8 生物秒;这是完整草案的计划库存,不是已执行数量,也不是工期预算。
完整批量训练尚未启动。
## 下一步仍然缺少
- 把每个真实输入、delta-before/after、状态和队列绑定外部 SHA 与祖先记录的完整协调器。
- 在指定 4090 上预先登记有限输入/更新衔接验证,实测完整种子、参考一致性和实际开销。
- 完成反转与连续 retention 的功能门实现,并核算完整存储、时间上限和停止条件。
- retention 还有明确接口缺口:当前冻结续跑器要求子段 overlay 与父检查点完全相同。
最后训练 epoch 的末态属于 delta-before,边界更新却产生 delta-after;不能把这组
“原神经末态 + 新权重”伪装成已经验证的同权重续跑。必须在新文件中明确实现并有限验证
更新边界的状态交接,不能修改封存续跑器来绕过约束。
- 以上通过后才登记执行 acquisition 及全部控制;若失败,保留失败证据,不通过改参数补救。
全部新增科学代码仍是 DRAFT;源文件清单与测试记录保存于
`models/male-cns-lif/olfactory-conditioning-lab-v1/draft-source-inventory.json`。
该记录不是实验注册或目录封存。既有 21 个 AN 注册源按其原清单单独核验,不纳入新草案清单。
不运行游戏、RL、decoder、视觉、Web 或 3D;不改 frozen reference/runtime、图、符号、
W_syn、APL 标定、eligibility-v2 或 14,232-edge mask。
@@ -0,0 +1,68 @@
# Explicit evaluation policy — draft implementation decision
Status: **DRAFT / NOT REGISTERED / NO SCIENTIFIC EXECUTION**.
This is a pre-outcome implementation choice by the coordinator for the existing
conditioning proposal. It does not change a frozen brain, eligibility-v2,
anatomical mask, odor identity, five-replicate matrix or scientific pass gate.
The AN engineering result remains separate; none of its 21 sources is edited.
## Probe timing and snapshots
- Every evaluation probe lasts 120 ms, with CS requests only in the half-open
interval [0, 100 ms). The remaining 20 ms is a no-CS tail. This matches the
proposed normal training CS duration; it is not the old L full-120-ms input.
- Normal, delayed and reversal curves use 0, 1, 2, 5, 10, 20 and 50 scheduled
reinforcement opportunities. Intermediate snapshots are taken after the
**complete epoch containing** that opportunity and its boundary update,
not immediately at the US tick.
- Terminal checkpoint 50 includes all planned epochs, including trailing
neutral or blank epochs. It is not an early snapshot chosen for response.
- Each snapshot executes both A and B evaluation inputs. No baseline-output
deduplication is assumed. Repeated probes use the same independent evaluation
input within a training replicate; they are paired observations, not new
training replicates. There are still five independent training replicates
per experimental cell and 80 independent A/B evaluation streams overall.
- Probe US and weight updates are disabled. A probe cannot update the training
or retention trajectory. Acquisition/reversal/delayed start from the common
declared reset state; retention forks the actual continuous saved state.
## Intermediate control alignment
For normal and delayed experiments, obtain the paired arm's complete ordered
CS prefix at the chosen epoch boundary. Match actual cue, occurrence, odor,
source random stream and full seed, not just the number of epochs.
- D=0, frozen, random reward and CS-only use that identical CS prefix.
- DAN-only uses the corresponding removed-CS/yoked epoch positions. It has
**zero CS exposure**, not equal CS exposure; this is an intended control.
- Unpaired uses the earliest epoch boundary that has consumed the same ordered
CS prefix, including US-only epochs already inserted before that boundary.
Its terminal checkpoint includes the entire 150-epoch inventory.
Unpaired intermediate US counts and elapsed time can differ from paired.
Random-reward intermediate US counts can also differ. These differences must
be reported rather than called fully matched exposure. All complete terminal
inventories remain fixed by the original proposal. The terminal gate, not the
most favorable intermediate point, decides the result.
Reversal and no-reversal use each arm's own kth reinforcement epoch boundary.
Their intermediate CS prefixes may differ because their reinforced cues differ;
their complete 100-epoch inventories remain fixed. The signed readout always
keeps the original acquisition cue orientation and original pre baseline.
## Inventory, not approval
Under these choices the pure planner is expected to enumerate 9,440 probes
(1,132.8 biological seconds), in addition to the existing 10,760 seconds of
training, controls, reversal and continuous idle. This arithmetic is not a
measured wall-time or disk estimate and excludes engineering validation,
encoding, output, regressions and reserves. Test fixtures use synthetic masters;
none is adopted as a scientific master seed.
Before any scientific run, complete the actual input/state/delta file bindings,
exact event-to-eligibility update coordinator, implemented assay gates and a
finite resource plan. Register the protocol and bounded implementation check
before observing scientific outcomes. Only after those checks may the staged
acquisition suite run. A failed acquisition is sealed as failure; it must not
trigger more pairings, parameter changes, odor reselection or control removal.
@@ -0,0 +1,84 @@
# Phase 4AN:条件化接口的生物学证据与解释边界
状态:**未注册 DRAFT;不是实验结果、科学 PASS 或学习候选声明。**
日期:2026-09-30(Asia/Shanghai)。
本文件记录为后续协议准备的一手文献核对。未修改冻结的脑、符号策略、4J eligibility-v2、mask、eta 或受体假设;未运行远端实验。文献证据不能代替新实验的完整门禁。
## 1. 本地身份、文献类型和具体个体必须分层
冻结 H 的静态身份依据是 `annotation_class`、`annotation_type`、`annotation_instance` 中精确的 compartment token,以及真实 KC→MBON 和 DAN→MBON 边;不是根据 PAM/PPL1 前缀猜测效价或 compartment。可读源码依据为 `../models/male-cns-lif/phase4h.yaml` 的 `audit`/`conditioning` 字段、`phase4h.py` 的 mask/confirmed-DAN 加载,以及 `phase4h_reinforcement_provenance.json`。
下表本地索引、body ID 和侧别是本轮协调者从冻结 H mask/metadata 的实际静态查询确认的目标集合,**不是文献记录的实验个体**。对应冻结 H 文件为 `/data/digital-drosophila/experiments/phase4h-dopamine-plasticity/plastic-mask/plastic-edge-candidates.parquet`,协调者提供的 SHA256 为 `e5075ee3e5a0edf3a4d197453847163cc295b254f21c67203159bba204b12a76`。本次文献核对没有重新下载该表。正式注册仍须将完整静态映射、来源路径及 SHA 与 mask 一同绑定;不得把本表作为绕过静态核验的硬编码替代。
| 本地类型与精确 token | 固定本地 neuron_idx | 已提供的本地 body ID | 文献类型对应 |
| --- | --- | --- | --- |
| MBON01 / `y5B'2a` | `9, 130579` | `9 → 10013`;`130579 → 520151` | MBON-01 = MBON-γ5β′2a |
| MBON12 / `y2a'1` | `130136, 131044, 3477, 3513` | `130136 → 519368 (R)`;`131044 → 521086 (R)`;`3477 → 13728 (L)`;`3513 → 13768 (L)` | MBON-12 = MBON-γ2α′1 |
文献编号与名称的对应来自 [Aso 等,2014,解剖论文 Table 1](https://elifesciences.org/articles/04577/figures)。ASCII token 与希腊字母类型名的对应是本项目跨数据集映射层的一部分,不证明每个 MaleCNS body 的生理效应、性别适用性、行为效价或受体组成。文献不能提供这些本地 neuron_idx/body ID 的身份核验。
冻结 mask 仍为 14,232 条边、四个精确 compartment;另外的 MBON07/`a1` 四个细胞和 MBON02/`B2B'2a` 两个细胞不会因为是 mask 成员而被自动赋予与下面两组相同的行为功能,也不因本次聚焦而从 mask 删除。
## 2. MBON01 与 MBON12 的一手功能证据
### MBON01:avoidance-related 类型,不等于每个本地细胞已验证
Stahl 等在同一条件化实验框架中测得,食欲条件化后 MBON-γ5β′2a 的 CS+ 相对 CS− 响应降低;论文将该 compartment 的输出联系到 avoidance-promoting MBON。该结果支持“减少这一类型的条件气味输出可能产生 appetitive-like 偏移”,不证明所有条件下它都是单一行为开关。[Stahl 等,2022,Figure 5 及 Figure 5—figure supplement 1](https://pmc.ncbi.nlm.nih.gov/articles/PMC8956283/)
较早的光遗传行为实验包含表达于多个 M4/M6 MBON 类型的驱动系;不能把多类型联合激活结果重述为每个 MBON01 个体的独立因果验证。[Aso 等,2014,行为论文 Table 2 与 Figures 2–5](https://cdn.elifesciences.org/articles/04580/elife-04580-v1.pdf)
### MBON12:approach-related 输出与 aversive-learning depression
Aso 等明确报告激活 MBON-γ2α′1 可引起趋近。Berry 等在气味与电击配对后观察到该 MBON 对 CS+ 的响应特异性抑制,未配对/气味和 US 分离的控制不产生相同结果;同一 DAN 的后续刺激还可恢复已抑制响应。因此这里有类型特异的趋近相关输出和厌恶记忆证据,但不支持“DAN 的任何刺激只会继续 LTD”。[Aso 等,2014,行为论文 pp.26–27](https://cdn.elifesciences.org/articles/04580/elife-04580-v1.pdf);[Berry 等,2018,Figures 2–4](https://pmc.ncbi.nlm.nih.gov/articles/PMC6239218/)
Stahl 等还观察到食欲条件化后 MBON-γ2α′1 的相对 CS+ 响应提高,但不能明确全部归因于 CS+ potentiation 或 CS− depression;不能用该结果论证所有相关 plasticity 都是单向 LTD。[Stahl 等,2022](https://pmc.ncbi.nlm.nih.gov/articles/PMC8956283/)
## 3. 两个人工 US 接口只具有预先声明的 like 解释
| 固定的源类型与 compartment 接口 | 类型层面的预期 | 不可升级成的结论 |
| --- | --- | --- |
| PAM15 → `y5B'2a` / MBON01 | 若目标气味的相关 KC 输入发生 LTD,可预期 avoidance-related 输出相对减弱,即 appetitive-like 输出偏移 | PAM15 全体天然奖励、所有 PAM 为正效价、这些本地细胞已经证明奖赏学习 |
| PPL103 → `y2a'1` / MBON12 | 若目标气味的相关 KC 输入发生 LTD,可预期 approach-related 输出相对减弱,即 aversive-like 输出偏移 | 所有 PPL1 为负效价、DAN 任意刺激均是惩罚、这些本地细胞已经证明厌恶学习 |
**PAM15 不是 PAM01 的同义词。** Li 等把 PAM15(γ5β′2a)列为相对既有分类新增的一类;PAM01 对应 γ5,PPL103 对应 γ2α′1。旧文献中 PAM-γ5/PAM01 的全部奖励结果不能不经核对就转记为 PAM15 特异性证据。[Li 等,2020,DAN 分类、Figure 6 与 Figures 35–36](https://pmc.ncbi.nlm.nih.gov/articles/PMC7909955/);[同文图表页](https://elifesciences.org/articles/62576/figures)
这里的 D 仍是冻结规则中的人工标量命令,不是测得的 dopamine 浓度,也不是由实际 DAN spike/接受刺激自动推定的生物学门控。命令、刺激是否被接受、实际 DAN events、选定 compartment 与权重更新必须分别记录。两个接口共用既定 polarity = −1;不因为接口名字不同而翻转 D、eta、base sign 或下游权重。
## 4. 顺序和时间窗证据不等于冻结参数的数值标定
- Hige 等在 γ1pedc 测得正向配对后的抑制;气味改为在最后一束 DAN 激活光后 0.5 秒开始的 backward protocol 未产生同样变化。这是特定 compartment 和刺激协议的结果,不是所有类型的通用截止时间。[Hige 等,2015,Figure 1](https://pmc.ncbi.nlm.nih.gov/articles/PMC4674068/)
- Aso–Rubin 显示,单一 DAN 类型可因相对气味的刺激时序不同而写入、削弱或改变记忆效价;不同 compartment 的学习和更新规则并不相同。[Aso–Rubin,2016](https://pmc.ncbi.nlm.nih.gov/articles/PMC4987137/)
- Handler 等在其协议中测得 forward pairing 对应 depression、backward pairing 对应 potentiation,并观察到 PAM/PPL 群均可随时序改变行为偏移方向。这阻止我们把 PAM/PPL 名称当作固定效价开关;本项目不据此加入新受体、LTP 或重设参数。[Handler 等,2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC9012144/)
出版年复核:该文正式题名为 *Distinct Dopamine Receptor Pathways Underlie the Temporal Sensitivity of Associative Learning*,刊于 **Cell,2019-06-27,178(1):60–75.e19**,online publication 为 2019-06-20,DOI 为 [10.1016/j.cell.2019.05.040](https://doi.org/10.1016/j.cell.2019.05.040)。[PubMed 原文书目记录 PMID 31230716](https://pubmed.ncbi.nlm.nih.gov/31230716/) 明确同时关联 PMC9012144。因此本草稿保留 2019;旧材料中的 2022 不是本论文正式出版年,本次不修改旧冻结材料,也不猜测该旧标注的来源。
冻结 4J eligibility-v2 的 τ = 100 ms、trace cap = 1、eta = 0.00275 mV、polarity = −1、最多 25% depression 仍是**既定 causal-LTD 工程假说**,不是上述论文为当前全脑提供的定量标定。负时序零更新和 +1000 ms/零延迟比例门只验证冻结规则的一致性;不能据此声称复现了生物学 relief learning、受体机制、bidirectional plasticity 或遗忘。4J 中另列的 bidirectional branch 也不是本接口可偷偷启用的替代规则。
## 5. 固定 B/S:新的离线分析约定,不是 decoder
首要输出应保留每个目标细胞的原始 spike/count/rate、训练前后变化,以及两个类型各自的结果。可供正式协议审议的固定类型对比为:
```text
R12(o) = mean of the fixed four MBON12 cells' rates for odor o
R01(o) = mean of the fixed two MBON01 cells' rates for odor o
B(o) = R12(o) - R01(o)
DeltaB(o) = B_post(o) - B_pre(o)
S = DeltaB(CS+) - DeltaB(CS-)
```
所有项使用预先锁定的同一评估窗,分别按类型取均值,避免四个 versus 两个细胞的数量差异直接决定符号。CS+/CS− 的角色须事先指定并 counterbalance,不能根据结果挑选气味方向、细胞或 seed。
这是**新的、需要预注册的离线分析约定**;不是文献给出的定量行为公式,不进入动力学、不拟合权重或阈值、不构建选择策略、classifier 或 decoder,也不替换旧封存指标。它不要求未训练 B = 0,不把 spike rate 等同于真实选择概率。
两个接口永远使用同一个 B 方向:PAM15 分支的假说是 S > 0,PPL103 分支的假说是 S < 0。不得按接口乘效价符号把所有结果改成“正值即成功”,不得取绝对值掩盖方向错误。若反馈使方向相反,应报告为该假说的反证,不改读出或重命名接口。还须与实际运行的预注册控制比较;单独 S 的方向、MBON separation 增大或权重改变都不构成完整 learning PASS。
## 6. 正式协议不可省略的解释门
- 文献类型功能、本地精确身份、人工 US 命令、实际 DAN 激活和实际 KC→MBON 更新须为不同证据字段;任何一层未知都不得用另一层填补。
- 冻结脑与 v2 不变;不得新设 receptor sign、改变 eta、扩 mask、选择最有利 compartment、挑训练 checkpoint 或把 direct/static25 诊断作为条件化本身。
- paired、unpaired、zero-D、frozen、CS-only、DAN-only 应为真实执行控制,不是 summary 中的标签;独立评估 seeds 和预先锁定的 cue 角色不能在看到结果后改变。
- 必须区分 weight update、实际 MBON 响应变化、类型对比方向和下游功能传递。纯 separation 可能在错误方向上增大;“appetitive-like/aversive-like”不能在没有对应证据时升级为行为学习结论。
- 本文件不新增 success threshold;完整通过条件应由尚待注册的协议明示,并保留失败、无效应和反向结果。没有训练结果就不生成 learned candidate。
浏览核对仅采用上列原始研究的期刊、官方 PDF 或 PMC 全文。没有把综述、搜索摘要的二次推断、Google FlyBody 或其他身体运动项目作为本阶段学习机制证据。
@@ -0,0 +1,469 @@
# DRAFT — fixed-v2 olfactory conditioning in the independent D18 branch
**Status: DRAFT / NOT REGISTERED / NOT APPROVED FOR EXECUTION.**
This document is a proposal for review. It does not authorize a run, record a
successful gate, freeze a protocol, or generate a learned candidate. It was
written from frozen H/J/L source and the declared AM interface contract, not
from selecting favorable AM outcomes. The coordinator must approve the final
scope, budget, source/input inventory, and exact gate implementation before
registration. No training or remote action is performed by this document.
## 1. Purpose and entry conditions
The next scientific endpoint should be actual acquisition under the unchanged
TemporalEligibility-v2 rule, followed, if its registered acquisition gate passes,
by fixed reversal, delayed-reward, and real-time retention assays. File validity,
static 25% sensitivity, or a change in delta alone is not learning.
Entry requires the **complete** AM whole-brain interface gate, including its
registered zero-overlay comparisons, nonzero internal/repeat validation, old
reference regression, and frozen integrity checks. A partially completed zero
suite is not this gate. Continue to use the independent D18 branch; do not edit
or overwrite Phase 1–AM artifacts. The original base CSR, anatomical counts,
transmitter signs, W_syn, LIF coefficients, APL calibration, D18 timing,
14,232-edge candidate mask, and eligibility-v2 remain unchanged.
New long-run/continuation plumbing must preserve the exact existing timestep
operation order. Validate zero-delta and fixed-delta continuation, including a
split across pending synaptic/APL arrivals, against an unsplit run before using
it for retention. This is an implementation prerequisite, not another tunable
scientific endpoint. No game, decoder, RL, visual branch, or brain-gain search
is in scope.
## 2. Fixed population and replicate matrix
Use the original externally selected 4L odors without re-selection:
* ODOR_A: **4-ethylphenol**.
* ODOR_B: **3-hexanone**.
* Both frozen encoder modes: `physiological` and `matched_energy`.
* Both reciprocal assignments: A reinforced/B neutral, and B reinforced/A
neutral.
* Both exact DAN/type-compartment branches below.
* Five independent training replicates in each cell.
Thus there are **2 modes × 2 DAN branches × 2 assignments × 5 replicates =
40 blocks**, or eight experimental cells with five training replicates each.
| Branch identity | Exact raw compartment string | Proposed signed prediction |
| --- | --- | --- |
| PAM15 | `y5B'2a` | S > 0 |
| PPL103 | `y2a'1` | S < 0 |
These identities are drawn from old H/J configuration, not from a rule that all
PAM is appetitive or all PPL1 is aversive. Preparation must materialize and hash
the actual DAN body IDs/neuron indices, exact annotation values, confirmed
DAN→MBON/compartment mapping, and affected existing plastic edges from the
sealed H mapping. Missing, ambiguous, or unmatched mappings remain `unknown`;
they cannot be completed by cell-name inference.
The type-level signed predictions above are **new, falsifiable engineering
hypotheses**. Evidence specifically establishing PAM15's proposed functional
direction is limited. Correspondence of type labels across fly datasets is not
proof that these individual MaleCNS bodies have been functionally validated.
Neither branch is labeled a biologically demonstrated reward/aversion channel.
### Seed inventory
Before outcomes, generate and save the complete RNG lineage and explicit seed
table for replicate, cell, arm, epoch, CS events, US ordering, and evaluation.
Use distinct replicate streams, for example an explicitly registered
`SeedSequence` hierarchy with a newly audited master seed. Test expanded seed
identities for collisions. Merely choosing five different base integers is
insufficient: H/J/L used consecutive bases plus the trial number, producing
overlapping nominal replicate streams.
Matched counterfactual arms may deliberately reuse the same input event table;
list each such common-random-number alias. They are paired controls, not extra
independent replicates. All evaluation RNG streams must be disjoint from all
training/US/control RNG streams. Within a replicate use the same registered
evaluation events for pre/post/control comparisons. If more than one evaluation
stream is used per replicate, report/aggregate it within that replicate;
**five evaluation streams never substitute for five independent training
replicates**. The minimal budget below uses one independent A/B evaluation
stream pair per training replicate, reused for paired contrasts.
## 3. Frozen update and explicit US interface
Use the exact frozen J/L v2 implementation and operation order:
* KC trace float32, tau = 100 ms, cap = 1.
* Every timestep: separate multiply, add actual KC spikes, then clamp.
* At the registered US tick, sample after that tick's multiply/add/clamp;
include its KC spikes. Do not insert an anatomical-delay shift into eligibility.
* eta = 0.00275 mV, compartment polarity = −1.
* Additive update only on the already-confirmed compartment mask, with the
old float32 floor `−0.25 × base_candidate_mV`.
* Update once at the end of the training epoch; it affects subsequent epochs,
not an earlier part of the same epoch. No recovery, potentiation, or new mask.
* Install via the verified AM bridge: immutable base SI float64 plus the
float32 delta widened to float64 and converted from mV to V. Preserve all
untouched weights and fast-APL handling exactly.
The first proposal preserves the old **scheduled dimensionless D = 1** artificial
unconditioned-signal interface. It is neither measured dopamine concentration
nor an actual-DAN-spike count. At the same scheduled tick, request spikes in the
fixed matched DAN population. Record requested, accepted, rejected, and actual
DAN spikes, including zero-delivery epochs, separately from D. Do not resample,
boost, or silently discard failed delivery to obtain a preferred result.
Multiplying D by actual acceptance, changing D to a spike count, or adding a
measured dopamine kinetic model would be a distinct, explicitly registered
interface hypothesis; none is hidden in this draft. If neural DAN delivery is
absent, any update still demonstrates scheduled artificial gating, not that
actual DAN activity caused plasticity. This limitation must remain visible even
if an engineering output gate passes.
## 4. Epochs, timing, and all actual controls
Training epochs retain the old trial-boundary reset policy: start V/g/ref,
spike-delay queues, APL state/queue and KC trace from their declared initial
values; carry only plastic_delta between epochs. Log the state policy. This is
not continuous natural behavior. Retention deliberately uses the different,
explicit continuous-state policy in section 7.
Normal epochs last 120 ms. CS occupies 0–100 ms, and a paired US is at 90 ms.
Delayed epochs last 180 ms; the identical CS occupies 0–100 ms, and US is at
150 ms, i.e. **50 ms after CS offset** and 60 ms later than the normal US.
All delayed controls use that same 180 ms duration and 150 ms US slot, including
the neutral epochs; do not reuse 90 ms controls to attribute delayed effects.
The frozen L encoder mapping/rates and original A/B identity are retained.
However, old `phase4l.py::run_rates` generated PN events throughout its passed
120 ms duration. The present 100 ms CS plus a no-CS tail is an explicit new
experimental timing choice aligned with validated AK input duration, **not** a
claim of bitwise identity to old L's full training input. `matched_energy`
matches the frozen encoder's expected total rate, not an artificially edited
actual Poisson event count; report actual requests and acceptance by condition.
Within each arm, present 50 reinforced-cue epochs and 50 neutral-cue epochs in
a pre-generated balanced order. The **50 reinforcement opportunities** come
from L's fixed formal conditioning count, not from a new magnitude search.
| Arm | Actual execution in each normal or delayed suite |
| --- | --- |
| paired | 50 CS+ with its matched DAN/US and 50 CS− without US |
| D=0 | Identical input events and US schedule to paired; D=0 in updates |
| frozen | Identical input events and US schedule; no delta updates |
| random reward | Same 50 A/50 B and 50 US opportunities; exactly 25 A and 25 B get US, selected by a pre-generated random balanced schedule independent of responses |
| unpaired | Same 100 CS epochs and same 50 US events in 50 additional separate, no-CS epochs; preserve total event inventory, declare 150 rather than 100 epochs |
| CS-only | Same 100 CS epochs, no DAN/US; no scheduled D |
| DAN-only | Same 50 US epochs and 50 matched blank epochs, no CS |
Every control is genuinely executed; neither identity reasoning nor a text label
replaces its trajectory. The unpaired arm is specifically **separate-epoch,
reset-trace unpairing**, not a claim about a long continuous biological
interstimulus interval. Actual DAN-only network activity may recruit KC and
produce eligibility; it must be measured, not assumed zero. Blank and US-only
epochs retain the corresponding 90/150 ms timing slot and 120/180 ms duration.
## 5. Offline signed readout — do not discard contrary signs
Preparation defines complete, exact-annotation MBON12 and MBON01 populations
from MaleCNS metadata, with all body/index membership and side information. No
best responding neuron, hemisphere, compartment subset, or evaluation pair is
selected after outcomes. Missing either population makes this specific readout
`NOT-TESTABLE`; do not substitute another type or silently use an empty mean.
For an evaluation stimulus X and a fixed 0–120 ms probe window, define:
`B(X) = mean_spike_count(MBON12, X) − mean_spike_count(MBON01, X)`.
This is **offline analysis only**, not an action decoder, a feedback signal,
or proof of approach/avoidance. Also save both population means separately,
their sizes and counts, all-MBON output, genuine downstream output, and latency.
Keep two distinct downstream inventories. The frozen H actual-edge table's
complete target union is `legacyDownstream` (11,342 neurons in the verified
local data), and contains **all 97 MBONs**. Do not relabel or silently edit that
old population. Define `nonMBONDownstream` as the exact set difference of that
union and the complete MBON population (11,245 here). Report both. Requiring
an effect outside the MBON population is an additional pre-outcome hypothesis,
not an already inherited H gate; it prevents a purely MBON-local change from
being counted twice as evidence of broader propagation.
With C denoting that block's reinforced cue and N its neutral cue:
`S_arm = [B_arm(C) − B_arm(N)] − [B_pre(C) − B_pre(N)]`.
Always emit the raw signed B and S, pre/post components and control S. For PAM15
the proposed relation is **S > 0**; for PPL103 it is **S < 0**. Never replace raw
S in the report with `abs(S)`, `polarity × S`, a magnitude-only score, or a
renamed “positive learning score.” An opposite sign is a counterexample to the
prediction, even if a magnitude or unsigned separation statistic increases.
The expected-control relation is equally explicit:
* PAM15: `S_paired > S_control` for each registered control.
* PPL103: `S_paired < S_control` for each registered control.
Use matched evaluation inputs and each arm's matched pre baseline. All probe
updates and US inputs are off. Evaluation begins from the same frozen initial
state for acquisition/reversal/delayed comparisons; retention uses its saved
continuation state and time-matched frozen control as specified below.
## 6. Acquisition and strict reversal gates
### 6.1 Acquisition — newly proposed signed gate
For each of the eight cells, a successful training replicate must satisfy all:
1. The raw S has the registered direction (PAM15 >0; PPL103 <0).
2. It lies in that direction beyond **each of the six actually executed
timing-matched controls**, not just the easiest control.
3. The inherited L functional requirement holds: conditioned-cue MBON
`Delta_g_max_abs` exceeds neutral-cue `Delta_g_max_abs`, and the conditioned
cue has a nonzero MBON spike-count or first-latency change versus its own pre
probe. Undefined latency is separately reported, not silently zero-filled
into proof of a latency effect.
4. The inherited H MBON and complete `legacyDownstream` A/B Euclidean
separation gains are positive. Independently require positive separation
gain in `nonMBONDownstream` as a newly declared specificity/transfer gate.
Report all three underlying count vectors and gains, never select whichever
downstream definition is more favorable after outcomes.
Require **at least 3 of the 5 independent training replicates to satisfy the
entire conjunction in the same replicate**. Also calculate the inherited H
mean-control rule separately. This exact new combined signed gate is a proposal,
not a claim that H/L already implemented it.
Record acquisition at fixed update counts 0, 1, 2, 5, 10, 20 and 50. Only the
registered terminal count 50 determines its gate. Do not stop at the best
earlier count or add pairings after failure.
### 6.2 Reversal — new hypothesis, not H's permissive sign test
After normal acquisition, fork its exact delta into two arms:
* **Reversal:** 50 pairings of the former neutral cue with the same specific DAN
branch, and 50 formerly reinforced cue epochs without US.
* **No-reversal continuation:** 50 further original-cue pairings and 50 original
neutral epochs, with matched epoch totals and registered independent input
streams. This is an additional real control, included in the budget.
Never reset the learned delta between acquisition and reversal; do not restore
depressed old-cue edges. Keep the old epoch-reset neural state policy in both
arms and evaluate from common fresh states. The pure-LTD/no-recovery rule may
prevent reversal; that is a falsifiable model limitation, not permission to add
potentiation, recovery, or more trials.
For strict reversal, retain the original C−N orientation when calculating
`S_acquisition`, `S_reversal`, and `S_continuation` against the **same original
pre-training baseline**. At the fixed terminal reversal count 50 require:
| DAN branch | Acquisition | Reversal | No-reversal continuation |
| --- | --- | --- | --- |
| PAM15 | `S_acquisition > 0` | `S_reversal < 0` | `S_continuation > 0` |
| PPL103 | `S_acquisition < 0` | `S_reversal > 0` | `S_continuation < 0` |
Additionally require a nonzero change in the former neutral cue's MBON spike
count or defined first latency between acquisition and reversal, and a nonzero
change in its actual-downstream event/count output. Save both cue vectors;
counts alone can conceal a shift confined to the original cue. The same
replicate must satisfy all conditions, with a 3/5 majority per cell. These are
**new terminal reversal hypotheses**, fixed before the learning results.
Zero is failure of a strict sign-crossing gate, not reversal. Report H's original
`sign(acquisition) != sign(reversal)` separately as a legacy statistic; its
nonzero→zero success case does not satisfy this new gate. Do not claim that
these engineering sign changes establish reversal of real appetitive/aversive
behavior.
## 7. Delayed reward and genuine retention gates
### Delayed-reward test — new hypothesis
Run delayed acquisition from zero delta, using the same fixed 50 reinforcement
opportunities and all six controls in the **180 ms / US-at-150 ms** suite. Sample
the unchanged trace at 150 ms after that tick's update. Do not extend tau or
redefine the trace to force delayed learning.
At count 50, require the same signed, functional, downstream and control
conjunction as section 6.1, with at least 3/5 independent training replicates per
cell and raw S direction intact. This explicitly tests whether the unchanged
rule supports this particular delayed schedule. H did not supply an independent
delayed-reward success threshold; this is a new pre-outcome gate.
Report delayed and immediate signed S side by side and their per-replicate
difference. Do not require a invented retained percentage, impose a monotonic
amplitude ratio, pool their seeds, or call an opposite delayed sign a successful
larger absolute response. Delayed controls cannot be substituted by normal ones.
### Retention — new continuous-state implementation and gate
Fork the terminal normal-acquisition checkpoint and the matched terminal normal
frozen-control checkpoint. Each includes V/g/ref, pending spike-delay state,
APL state, the complete D18 APL queue, absolute tick, delta, and relevant RNG
state. Run real no-input/no-update dynamics continuously to 1 s and 10 s; the 1 s
checkpoint is on the same idle trajectory used to reach 10 s. Do not reset,
discard pending events, analytically decay only delta, or fabricate elapsed time
by changing a seed.
At 0, 1, 10 s, fork independent A/B probes from each checkpoint; probes do not feed
back into the idle continuation. Use the same registered evaluation streams
for trained/frozen comparisons at every elapsed time. The probe lasts 120 ms,
without US or updates. Keep delta constant throughout idle; the model does not
contain molecular consolidation or recovery, and a pass cannot prove either.
Define signed `S_trained(t)` and `S_frozen(t)` using the original C−N orientation
and their registered pre baselines. Require, in the **same replicate at both 1 s
and 10 s**:
* PAM15: `S_trained(t) > 0` and `S_trained(t) > S_frozen(t)`.
* PPL103: `S_trained(t) < 0` and `S_trained(t) < S_frozen(t)`.
* Conditioned-cue MBON output has a nonzero spike-count or defined first-latency
difference from the time-matched frozen probe; actual-downstream event/count
output also differs. Save neutral-cue differences and the complete signed
specificity contrasts so a generic global response is not presented as memory.
Require 3/5 independent training replicates per cell, without a fitted amplitude
tolerance or an arbitrarily chosen “percent retained.” This is a new functional
retention hypothesis, not the old H reset-and-retest result. Report an optional
predeclared fresh-state/persistent-delta probe separately as a weight-storage
diagnostic; it cannot satisfy the real-time gate.
## 8. Legacy gates, new hypotheses, and candidate limits
Retain the following old predicates verbatim as separately labeled audit fields
where their data definitions apply:
* H: minimum seed majority 3; positive MBON and downstream Euclidean separation
gains; the smaller mean gain of its two DAN branches exceeds the maximum
mean gain among D0/random/frozen controls.
* H: legacy reversal sign-inequality and last-retention separation>pre. Disclose
the old retention had no actual idle dynamics and is not direct validation of
the new real-time protocol.
* L: conditioned `Delta_g_max_abs`>neutral plus a functional spike/latency change,
in at least 3/5 seeds. Preserve the exact old implementation's legacy result as
well as the new explicitly defined handling of missing latencies.
There was **no completed inherited delayed-reward gate**, no proven signed
MBON12−MBON01 valence mapping for these bodies, and no complete L reversal or
retention assay. The strict signed gates in sections 6–7 are new proposals, not
historical passes and not thresholds obtained from AL/AM responses.
For the strongest proposed engineering candidate claim, all eight cells must
pass their registered acquisition and subsequent assay gates, old required
integrity/control gates, and numerical safety checks. This all-cell conjunction
is itself an explicit proposed scope of claim; it is not a pre-existing H/L
aggregate gate. Emit a complete 8-cell×assay table, including contrary signs,
failures, `NOT-RUN`, `NOT-TESTABLE`, and actual delivery limitations. Do not pool
one mode/compartment/assignment to rescue another, or silently retain only
successful cells as the candidate.
Suggested staged execution: run normal acquisition and all six normal controls
for all 40 blocks first. If the approved overall acquisition gate fails, seal a
real acquisition failure and stop, recording subsequent assays as `NOT-RUN`.
Otherwise execute the already registered complete delayed, reversal,
continuation and retention suites. No gate changes, tuning or new pair choices
occur between stages. A scientific failure is not a reason to delete evidence;
an integrity/numerical/resource failure stops execution without a scientific
success claim.
Even a full engineering pass would not establish odor preference behavior,
motor learning, natural dopamine concentration, appetitive/aversive valence,
long-term memory, or readiness for a game. Delta change alone, a file/checksum
pass, static-overlay sensitivity, or signed output readout alone is insufficient.
## 9. Transparent worst-case runtime and data budget
The original estimate of 38,000 epochs omitted the new no-reversal continuation
and a full timing-matched delayed-control suite. They cannot be silently added
without revising the budget.
| Per block | Epochs | Biological seconds |
| --- | ---: | ---: |
| Normal paired + five 100-epoch controls + 150-epoch unpaired | 750 | 90 |
| Delayed paired + five 100-epoch controls + 150-epoch unpaired | 750 | 135 |
| Reversal | 100 | 12 |
| No-reversal continuation | 100 | 12 |
| Total training/control/continuation | 1700 | 249 |
Across 40 blocks this is **68,000 epochs and 9,960 biological seconds**. Real 10 s
idle progression of trained and frozen retention branches adds 800 s, totaling
**at least 10,760 s biological time** (107.6 million dt=0.1 ms steps), before
evaluation probes, implementation validation, and old regressions. Checkpoints
at 1 s are reused within the 10 s trajectory, not separate additional 10 s runs.
Relative to 38,000: add 4,000 continuation epochs and 26,000 delayed-control epochs.
All delayed neutral/control epochs are 180 ms, not shortened to 120 ms. The table
includes this duration correction. Any approved reduction of scope must be
decided and documented before registration, not after results or a resource
overrun.
Let measured AM walltime per 100 ms be `t100`. A preliminary linear conversion is
`107,600 × t100`, plus input generation, updates, evaluation, I/O and validation.
If `t100=1 s`, this base is 29.9 h; if 5 s, 149.4 h. These are arithmetic scenarios, not
claimed measured performance: learning may change spike load. Measure and
reserve a conservative actual budget before registration; enforce a resource
stop rather than changing the protocol during a run.
Full generated+applied APL traces alone use approximately
`2 × 4064 × 4 × 10000 = 325,120,000 bytes/biological-second`, hence **more than
3.18 TiB** for the above minimum. Do not silently carry the small AM experiment
disk budget into this suite. Per-epoch full neuron counts also total about 45 GB
at 68,000×166,700×4 bytes, before events or full checkpoints.
The proposed recording policy to approve before registration is: all spike
events, actual input/delivery logs, per-epoch counts, signed metrics, every delta
update and full terminal V/g/ref/APL/queue state. Every timestep retains a GPU
finite-value latch and first-failure tick, **not a CPU copy/hash of all state**.
At epoch boundaries hash the files and bind the complete chain of parent,
input, delta-before, events/state/queues, eligibility/D and delta-after. Full
APL/per-tick reference recordings are restricted to a fixed pre-outcome
validation inventory, not a result-selected subset. That subset's exact
comparison must not be described as per-tick regression of all 68,000 epochs.
Compute capacity from measured event/log
sizes plus a stated safety reserve, and register an exclusive-output storage
cap. Do not publish a guessed final compressed size or omit encoder/update/I/O
latency from walltime accounting.
The frozen L/J metric called `MBON_g_max_abs` is the maximum **across neurons
of the terminal post-minus-pre g difference**, not a temporal maximum. Save
`MBON_final_delta_g_max_abs` with explicit SI units and the legacy alias when
needed. Complete terminal g and sparse events suffice to reconstruct that
metric; adding a dense time-by-neuron recording is not required by its name.
Even without full histories, all 68,000 terminal state/ring payloads would use
roughly 507 GB uncompressed, before events and evaluation. Use lossless
compression and measure real complete-episode outputs before setting the hard
storage cap. Do not infer final compressed usage from sparse activity alone.
## 10. Evidence, provenance, and unresolved biology
Local source basis (read-only, relative to the project):
* `23-ai-gpu/source/models/male-cns-lif/phase4h.yaml` and `phase4h.py`:
exact DAN configuration, old control/majority predicates, old nominal retention.
* `23-ai-gpu/source/models/male-cns-lif/phase4j.yaml` and `phase4j.py`:
frozen float32 trace order and `apply_v2`.
* `23-ai-gpu/source/models/male-cns-lif/phase4l.yaml` and `phase4l.py`:
original odor mapping, 50 formal pairings, input timing, and acquisition gate.
* `23-ai-gpu/source/wholebrain-plastic-interface-v1/PROTOCOL.md`:
unchanged-base bridge, scheduled-US semantics and later-learning boundary.
Relevant primary evidence, with narrow use:
* [Hige et al., Neuron 2015, author institution](https://www.janelia.org/publication/heterosynaptic-plasticity-underlies-aversive-olfactory-learning-drosophila):
compartmental KC→MBON plasticity and odor-specific effects motivate local LTD;
this does not measure this MaleCNS model's eta, tau or DAN concentration.
* [Aso and Rubin, eLife 2016, author institution](https://www.janelia.org/publication/dopaminergic-neurons-write-and-update-memories-cell-type-specific-rules):
cell-type-specific DAN learning/update rules oppose a universal cluster-valence
assumption. They do not functionally identify every MaleCNS body here.
* [Aso et al., eLife 2014](https://elifesciences.org/articles/04580):
MBON population outputs motivate population-level readout. The exact
MBON12−MBON01 signed hypothesis and the two predictions in this draft remain
explicit cross-dataset/model assumptions, not a demonstrated behavioral scale.
The exact scalar D interface, fixed trace/eta/floor, absence of recovery,
trial-boundary resetting, simplified signed readout, and 100/120/180 ms timings
are declared model/engineering choices. In particular, the 1.8 ms anatomical
synaptic delay is not an eligibility time constant. Physiological applicability,
PAM15 functional direction in these bodies, continuous intertrial learning,
behavioral valence and long-term retention remain unresolved.
Before formal registration the coordinator must approve and hash the completed
population/seed/input inventory, new readout/gate code, exact continuation
semantics, storage policy/cap, resource budget and final candidate wording.
Until then this remains **DRAFT — NOT REGISTERED — NO EXECUTION AUTHORITY**.
@@ -0,0 +1,118 @@
# Olfactory conditioning lab — unregistered preparation
This directory is an independent, unfinished model-branch implementation.
Its 21 engineering sources are registered and sealed by Phase 4AN; those files
must not be edited. Phase 4AM and the finite Phase 4AN engineering gate passed.
The scientific conditioning proposal and additional planning modules remain
unregistered, have not run training, and have not produced a learned candidate.
Engineering PASS is not learning PASS.
Engineering evidence: `registry/phase4an-engineering-validation` in the project
repository, commit `aeccb7d0ccdcf6788e0a8de60e7ab96f70dbe734`. Its exact
21-source inventory is in `source-identity.json`; this README is not one of
those registered sources. The experiment manifest SHA-256 is
`e8952a48cd3ea6a93c811a1183a1f7b79e19646299924e7688e5f393737a9aa2`.
## Existing code
- `runtime_light.py`: derives a fixed-weight trial runner from the SHA-pinned
AJ source. Its full-recording reference retains the entire original tick
loop. The light variant removes an explicit diagnostics whitelist while
preserving the ordered mathematical AST. It retains sparse events, stimulus
acceptance, terminal neural state and delay queues. It supports an explicit
longer admission bound, not hidden time truncation or approximate delays.
- `seed_registry.py`: explicitly separates training, evaluation and schedule
design random streams. Declared same-split aliases can give controls common
random inputs; aliases do not count as independent replicates. Duplicate
keys, cross-split aliases and seed collisions stop preparation. A full
manifest is independently reconstructible.
- `runtime_continuation.py`: carries full terminal state, spike/APL delay
queues and the absolute clock across fixed-weight segments. It protects
every ancestor checkpoint against output/build writes. The explicit
arrival-admission change and reporting hooks are AST-audited. The finite AN
gate passed 32 actual GPU calls and 74 exact comparisons, including pending
arrivals across a split. This does not verify every future 120/180 ms
training epoch or the unexecuted 1/10 s retention protocol.
- `biological_contract.py`: binds observed canonical identity, exact DAN/MBON
compartment tokens and real graph edges without filling unknown relations.
- `trial_plan.py`: constructs and independently validates the proposed complete
40-block training/control matrix and explicit random-stream aliases. It has
not selected a formal master seed or generated scientific brain inputs.
Complete-protocol measured storage and runtime budgets remain unfinished.
- `evaluation_plan.py`: requires explicit evaluation timing, reversal points
and per-control snapshot bindings. It reconstructs the parent inventory and
retains 80 independent A/B evaluation streams, paired across snapshots within
each training replicate. It does not bind actual state files or authorize a run.
- `evaluation_policy.py`: supplies the explicit draft policy in
`EVALUATION_POLICY_DRAFT.md`: 100 ms CS, 120 ms probes, seven curve points,
actual ordered-CS-prefix control alignment and separate US/time accounting.
Its conditional inventory contains 9,440 probes, not executed trials.
- `conditioning_inputs.py`: consumes a full 63-bit semantic seed in the original
discrete float32 Bernoulli CUDA sampler, with explicit device/draw budget and
CS window. CPU/mock tests do not establish CUDA random-sequence equivalence or
the influence of high seed bits. It never substitutes a CPU random generator.
- `conditioning_update.py`: uses the SHA-pinned AM/J bridge to compute a single
epoch-end delta from supplied actual sparse events. It never installs weights;
scheduled D, actual DAN activity, attempted increments and actual changed
edges remain distinct. Synthetic validation does not establish a trained brain.
- `metrics.py`: pure, explicitly draft functional gates; keeps the historical
downstream population and the non-MBON difference as distinct readouts.
Caller-bound receipts and anatomical membership are still required.
- `test_*.py`: CPU synthetic/structural checks. These cannot establish real
GPU equivalence, biological validity or successful learning.
- `EVIDENCE_DRAFT.md` and `PROTOCOL_DRAFT.md`: evidence and protocol proposals;
neither is an approved or frozen experiment.
## Required before registration or training
1. Preserve completed AM/AN engineering evidence. AM completed its 144 zero
and 144 fixed-overlay runs, five old frozen reference trials, and integrity
checks. AN's 32-call continuation gate passed its finite exact comparisons
and 28-root integrity checks; it did not rerun the five old reference trials.
Neither gate executed conditioning.
2. Review and lock the complete experimental matrix, true executed controls,
separate training/evaluation stream table, data budget and stop rules.
Report any additional gates as new model hypotheses, not inherited facts.
The generic seed registry alone does not prove protocol coverage or semantic
independence: preparation must require every planned stream and permit
aliases only within the same training replicate and input role, for named
matched controls/pre-post comparisons. Never alias two training replicates
or CS and US roles. Save generator/version, target ordering, rates, timing,
consumed stream ID and actual requests hash. Do not truncate a 63-bit seed
to 32 bits or derive a new seed by adding the epoch number downstream.
3. Bind **all** graph arrays, APL bundle, coefficient JSON, base-candidate
vector, full 14,232-edge mask, DAN/MBON identity tables and input files to
the registered manifest. A runtime's self-consistent spec or source hash
is not sufficient authority for these data.
4. Preserve the completed finite real-GPU light/reference comparisons, including exact
ordered events, per-neuron counts, terminal V/g/ref, external-request
acceptance, APL terminal state and both pending delay queues. No tolerance
may excuse a spike divergence. Benchmark encoding, loading, simulation,
validation and output time separately and together.
5. Validate the actual retention protocol before claiming retention. The
separate continuation module passed the finite AN GPU gate. V/g/ref,
spike-ring phase, APL state/pending phase and global tick must survive;
segmented and monolithic executions must agree. A checkpoint seal binds
fresh state bytes when created; it cannot prove pre-seal immutability.
6. Verify every learned snapshot's parent chain, input events and exact
frozen eligibility-v2 update. The single-trial runtime verifies the
bounded overlay and parent-hash declaration, but does not prove the
coordinator actually computed that update. Within each neural trial the
delta is fixed. The original base is never written.
7. Record the realized wall-time/output budget before the full acquisition
suite. A failed acquisition gate must not be rescued by tuning eta,
thresholds, APL gain, trial count, mask membership or selected odors.
## Interpretation limits
PAM15 and PPL103 are specific, statically mapped artificial US interfaces;
PAM/PPL cluster names are not general valence labels. The fixed eligibility
rule is a single-direction causal-LTD engineering hypothesis, not a complete
model of dopamine, bidirectional plasticity, forgetting or consolidation.
The proposed fixed MBON type contrast is offline analysis, not a decoder.
No old sealed artifact, raw connectome, transmitter sign, eligibility-v2 or
base weight is modified here. No game, reinforcement-learning policy,
classifier, action decoder, visual encoder, web service or 3D interface is
introduced. A future learning PASS requires functional stimulus-specific
responses and executed controls, never merely a changed delta array.
@@ -0,0 +1,205 @@
"""Read-only exact-table contracts for the proposed conditioning branch.
No body identity, compartment or behavioral function is inferred from a name.
The specific two interfaces are explicit protocol hypotheses checked against
the frozen H maps and actual graph edges. No training or file write occurs.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
import numpy as np
import pandas as pd
INTERFACES = {"PAM15": ("y5B'2a", "MBON01"), "PPL103": ("y2a'1", "MBON12")}
N = 166700
E = 25582938
INPUT_PINS = {
"datasets/male-cns-v1.0/processed/neuron-only/neuron-index/unified-metadata.parquet":
"922af16846a065c3dbfca7c11146b83ca217fcbd7392edc745f444a04eae8c63",
"experiments/phase4h-dopamine-plasticity/plastic-mask/plastic-edge-candidates.parquet":
"e5075ee3e5a0edf3a4d197453847163cc295b254f21c67203159bba204b12a76",
"experiments/phase4h-dopamine-plasticity/mappings/dan-to-compartment.parquet":
"a16f72c3121ed0ec638c1a2b249ace446e6ccee020b49679818eb42589d00662",
"experiments/phase4h-dopamine-plasticity/mappings/mbon-to-compartment.parquet":
"54f371f7e35b9def44becd1ba9bd2872861f4876b255b2c8ec46feee6473b70c",
"experiments/phase4h-dopamine-plasticity/mappings/dan-to-mbon-confirmed.parquet":
"730d000f25667b59181d2ee6703c64591229a8beab0856197ed71e68d799d191",
}
def require(ok, message):
if not ok:
raise ValueError(message)
def digest(path):
h = hashlib.sha256()
with Path(path).open("rb") as stream:
for block in iter(lambda: stream.read(8 << 20), b""):
h.update(block)
return h.hexdigest()
def _columns(frame, names, label):
require(isinstance(frame, pd.DataFrame) and set(names) <= set(frame.columns),
"Missing observed schema columns: " + label)
def _identity(frame, meta, label):
_columns(frame, ("neuron_idx", "body_id", "annotation_type", "annotation_instance"), label)
require(frame.neuron_idx.dtype == np.int32 and frame.body_id.dtype == np.uint64,
"Canonical identity dtypes changed: " + label)
ids = frame.neuron_idx.to_numpy()
require(len(np.unique(ids)) == len(ids) and np.all((ids >= 0) & (ids < len(meta))),
"Ambiguous/out-of-range identity: " + label)
for field in ("body_id", "annotation_type", "annotation_instance"):
actual = frame[field].reset_index(drop=True)
expected = meta.iloc[ids][field].reset_index(drop=True)
require(actual.equals(expected), "Identity field mismatch: " + label + "/" + field)
def _edges(frame, meta, src, dst, signed_raw, label):
names = ("edge_id", "source_neuron_idx", "source_body_id", "target_neuron_idx", "target_body_id",
"raw_synapse_count", "source_annotation_type", "source_annotation_instance",
"target_annotation_type", "target_annotation_instance")
_columns(frame, names, label)
require(frame.edge_id.dtype == np.int64 and frame.edge_id.is_unique, "Invalid edge IDs: " + label)
ids = frame.edge_id.to_numpy()
require(np.all((ids >= 0) & (ids < len(src))), "Edge outside graph: " + label)
require(frame.raw_synapse_count.dtype == np.int64 and (frame.raw_synapse_count > 0).all(),
"Invalid anatomical contact count: " + label)
require(np.array_equal(np.abs(signed_raw[ids]), frame.raw_synapse_count.to_numpy()),
"Anatomical edge weight mismatch: " + label)
for side, expected_idx in (("source", src[ids]), ("target", dst[ids])):
require(frame[side + "_neuron_idx"].dtype == np.int32 and
frame[side + "_body_id"].dtype == np.uint64,
"Canonical edge identity dtype mismatch: " + label)
require(np.array_equal(frame[side + "_neuron_idx"].to_numpy(), expected_idx),
"Actual graph endpoint mismatch: " + label)
for field in ("body_id", "annotation_type", "annotation_instance"):
actual = frame[side + "_" + field].reset_index(drop=True)
expected = meta.iloc[expected_idx][field].reset_index(drop=True)
require(actual.equals(expected), "Actual edge identity mismatch: " + label + "/" + side + "/" + field)
def audit_interface_tables(meta, dan, mbon, confirmed, candidates, src, dst, signed_raw):
"""Pure table audit; small synthetic graphs are allowed in unit tests only.
The production loader below separately requires the complete N/E identity.
Supplied annotation population membership is never filled from a prefix.
"""
_columns(meta, ("neuron_idx", "body_id", "annotation_class", "annotation_type", "annotation_instance"), "metadata")
require(meta.neuron_idx.dtype == np.int32 and meta.body_id.dtype == np.uint64,
"Canonical metadata dtype mismatch")
require(np.array_equal(meta.neuron_idx.to_numpy(), np.arange(len(meta), dtype=np.int32)) and meta.body_id.is_unique,
"Metadata must have canonical complete ordered identities")
require(src.dtype == dst.dtype == np.int32 and signed_raw.dtype == np.int64 and
src.ndim == dst.ndim == signed_raw.ndim == 1 and len(src) == len(dst) == len(signed_raw),
"Graph endpoint/weight schema mismatch")
require(np.all((src >= 0) & (src < len(meta))) and np.all((dst >= 0) & (dst < len(meta))),
"Graph endpoint out of range")
_identity(dan, meta, "DAN map")
_identity(mbon, meta, "MBON map")
for table, klass, label in ((dan, "DAN", "DAN"), (mbon, "MBON", "MBON")):
require(np.array_equal(np.sort(table.neuron_idx.to_numpy()), np.flatnonzero(meta.annotation_class.eq(klass))),
"Mapping must include the complete exact-annotation population: " + label)
_columns(table, ("mb_compartment_verbatim", "mb_compartment_mapping_status"), label)
_edges(confirmed, meta, src, dst, signed_raw, "confirmed DAN-MBON")
_edges(candidates, meta, src, dst, signed_raw, "plastic candidates")
_columns(confirmed, ("source_compartment_verbatim", "target_compartment_verbatim", "exact_compartment_match"), "confirmed DAN-MBON")
_columns(candidates, ("target_compartment_verbatim", "plasticity_polarity"), "plastic candidates")
require(confirmed.exact_compartment_match.dtype == np.bool_ and confirmed.exact_compartment_match.all(),
"Confirmed table contains an unknown compartment relation")
require(confirmed.source_compartment_verbatim.notna().all() and
confirmed.source_compartment_verbatim.equals(confirmed.target_compartment_verbatim),
"Confirmed exact compartment relation differs")
require(meta.iloc[candidates.source_neuron_idx.to_numpy()].annotation_class.eq("Kenyon_Cell").all() and
meta.iloc[candidates.target_neuron_idx.to_numpy()].annotation_class.eq("MBON").all(),
"Candidate is not a real KC-MBON edge")
require((signed_raw[candidates.edge_id.to_numpy()] > 0).all() and candidates.plasticity_polarity.eq(-1).all(),
"Frozen KC base sign/polarity changed")
pop = {klass: np.flatnonzero(meta.annotation_class.eq(klass)).astype(np.int32)
for klass in ("Kenyon_Cell", "MBON", "DAN")}
reports = {}
for dan_type, (compartment, mbon_type) in INTERFACES.items():
dd = dan[dan.annotation_type.eq(dan_type)].sort_values("neuron_idx")
mm = mbon[mbon.annotation_type.eq(mbon_type)].sort_values("neuron_idx")
require(len(dd) > 0 and len(mm) > 0, "Missing exact type: " + dan_type)
for table in (dd, mm):
require(table.mb_compartment_mapping_status.eq("explicit_single_parenthesized_token").all() and
table.mb_compartment_verbatim.eq(compartment).all(),
"Unknown or mismatched compartment: " + dan_type)
edges = confirmed[confirmed.source_neuron_idx.isin(dd.neuron_idx)]
require(len(edges) > 0 and edges.source_compartment_verbatim.eq(compartment).all() and
edges.target_compartment_verbatim.eq(compartment).all() and
edges.target_neuron_idx.isin(mm.neuron_idx).all(),
"Specific DAN interface has unknown or mismatched actual target")
require(set(edges.source_neuron_idx) == set(dd.neuron_idx) and
set(edges.target_neuron_idx) == set(mm.neuron_idx),
"Incomplete actual DAN/MBON type coverage")
plastic = candidates[candidates.target_compartment_verbatim.eq(compartment)]
require(len(plastic) > 0 and plastic.target_neuron_idx.isin(mm.neuron_idx).all() and
set(plastic.target_neuron_idx) == set(mm.neuron_idx),
"Frozen plastic subset is not the exact mapped target population")
pop[dan_type] = dd.neuron_idx.to_numpy(dtype=np.int32)
pop[mbon_type] = mm.neuron_idx.to_numpy(dtype=np.int32)
def members(frame):
return frame[["neuron_idx", "body_id", "annotation_type", "annotation_instance", "mb_compartment_verbatim"]].to_dict(orient="records")
reports[dan_type] = dict(compartment=compartment, MBON_type=mbon_type,
DAN_members=members(dd), MBON_members=members(mm),
confirmed_DAN_MBON_edge_ids=edges.edge_id.tolist(),
confirmed_DAN_MBON_edges=len(edges), confirmed_DAN_MBON_contacts=int(edges.raw_synapse_count.sum()),
candidate_edges=len(plastic), candidate_contacts=int(plastic.raw_synapse_count.sum()),
unknown_selected_interface_relations=0,
functional_valence="cross-dataset type-level hypothesis, not measured MaleCNS body function",
artificial_US=True, D_equals_measured_dopamine=False)
return pop, dict(interfaces=reports, whole_DAN_mapping_unknown=int(dan.mb_compartment_verbatim.isna().sum()),
whole_MBON_mapping_unknown=int(mbon.mb_compartment_verbatim.isna().sum()),
all_candidate_edges_retained=len(candidates), populations={k: len(v) for k, v in pop.items()},
global_unknowns_filled=False, training=False, new_identity_inferred=False)
def _complete_graph_identity(meta, graph, n=N, e=E):
"""Exact full-graph dimensions; small explicit sizes are for unit tests."""
require(len(meta) == n and graph["nodes"].dtype == np.int32 and
np.array_equal(graph["nodes"], np.arange(n, dtype=np.int32)), "Complete canonical neuron set required")
require(graph["edge_id"].dtype == np.int64 and
np.array_equal(graph["edge_id"], np.arange(e, dtype=np.int64)), "Complete canonical edge set required")
require(all(graph[name].shape == (e,) for name in ("src", "dst", "signed_raw")),
"Complete graph endpoint/weight lengths required")
def load_bound_tables(root, graph_manifest):
"""Require external, registered graph hashes, not self-authorized paths.
graph_manifest maps the absolute graph paths to their authorized SHA-256.
The coordinator must bind this dictionary to the preregistration/parent.
"""
root = Path(root).resolve()
observed = {}
tables = []
for relative, sha in INPUT_PINS.items():
path = root / relative
require(digest(path) == sha, "Frozen table SHA mismatch: " + relative)
observed[str(path)] = sha
tables.append(pd.read_parquet(path))
meta, candidates, dan, mbon, confirmed = tables
graph_dir = root / "experiments/phase4ag-author-apl-r2/graphs/full"
graph = {}
for name in ("nodes", "edge_id", "src", "dst", "signed_raw"):
path = graph_dir / (name + ".npy")
require(str(path) in graph_manifest and digest(path) == graph_manifest[str(path)],
"Unbound/changed complete graph input: " + name)
observed[str(path)] = graph_manifest[str(path)]
graph[name] = np.load(path, mmap_mode="r", allow_pickle=False)
_complete_graph_identity(meta, graph)
require(len(candidates) == 14232, "Complete frozen mask required")
populations, audit = audit_interface_tables(meta, dan, mbon, confirmed, candidates,
graph["src"], graph["dst"], graph["signed_raw"])
require(all(digest(path) == sha for path, sha in observed.items()), "Input bytes changed during audit")
return populations, dict(audit, input_sha256=observed, schema={
name: table.dtypes.astype(str).to_dict() for name, table in
(("metadata", meta), ("candidates", candidates), ("DAN", dan), ("MBON", mbon), ("confirmed", confirmed))})
@@ -0,0 +1,221 @@
"""UNREGISTERED input consumer; no I/O, brain execution, or training.
The frozen L/AK interface is discrete float32 Bernoulli (one request per PN
and tick), not a continuous-time Poisson sampler. PN/rate mapping is supplied
and externally authenticated by the caller; this module never selects odors,
rebuilds mappings, rescales rates, clips probabilities, or resets brain RNGs.
generate_cs_requests lazily uses the original CUDA Torch random operation.
CPU mocks test its contract only, not CUDA sequence equivalence. A full
(steps, PN-columns) matrix is drawn once, then the explicit half-open CS
window masks requests. A 100 ms CS in a 120 ms epoch is NOT the full old L
120 ms input. Different draw shapes need not share identical random prefixes.
The caller must register source/rate authority, device/environment, budgets,
stream aliases, and a separate bounded CUDA equivalence test before real use.
Returned request/source arrays are owned read-only copies, not immutable
capabilities; the consumer must recheck their recorded hashes before use.
"""
from __future__ import annotations
import hashlib
import re
import numpy as np
N_NEURONS = 166700
L_SOURCE_SHA = 'e97d58d8c1f44920d0e953f8f5831ddc86fa0198784e86460e315de4116ac553'
AK_ENCODER_SHA = '0061689abb1832e963718bd836bc8a50009ee38b258143a753b4d59e63414925'
def require(value, message):
if not value:
raise ValueError(message)
def array_identity(value):
return dict(dtype=value.dtype.str, shape=list(value.shape), order='C-logical-bytes',
sha256=hashlib.sha256(value.tobytes(order='C')).hexdigest())
def _vector(value, dtype, name):
require(isinstance(value, np.ndarray) and value.dtype == np.dtype(dtype) and value.ndim == 1,
name + ' must be an exact one-dimensional ' + np.dtype(dtype).name + ' array')
def _targets(value):
_vector(value, np.int32, 'targets')
require(np.all((value >= 0) & (value < N_NEURONS)), 'Target outside canonical global neuron range')
require(len(np.unique(value)) == len(value), 'Duplicate PN columns rejected; never merge rates or deduplicate RNG columns')
def _clock(steps, start, stop):
require(type(steps) is int and 0 < steps <= np.iinfo(np.int32).max, 'steps must be a positive int32-range integer')
require(type(start) is type(stop) is int and 0 <= start <= stop <= steps, 'Invalid explicit half-open CS tick window')
def probabilities_f32(targets, rates_hz, *, dt_ms=0.1):
"""Preserve the L expression's separate float32 multiply then divide."""
_targets(targets); _vector(rates_hz, np.float32, 'rates_hz')
require(rates_hz.shape == targets.shape and np.all(np.isfinite(rates_hz)) and np.all(rates_hz >= 0), 'Invalid PN rate vector')
require(type(dt_ms) in (int, float) and dt_ms == 0.1, 'Only the frozen dt_ms=0.1 contract is supported')
probability = np.divide(np.multiply(rates_hz, np.float32(dt_ms), dtype=np.float32), np.float32(1000.0), dtype=np.float32)
original = rates_hz * dt_ms / 1000.0
require(original.dtype == np.float32 and original.tobytes() == probability.tobytes(), 'Installed NumPy changed the frozen rate expression')
require(np.all(np.isfinite(probability)) and np.all((probability >= 0) & (probability <= 1)), 'Probability outside [0,1]; no clipping or rate adjustment')
return probability
def forced_from_uniforms(uniforms, probability):
"""Pure arithmetic fixture helper, NOT a replacement CUDA generator."""
_vector(probability, np.float32, 'probability')
require(isinstance(uniforms, np.ndarray) and uniforms.dtype == np.float32 and uniforms.ndim == 2 and
uniforms.shape[1] == len(probability), 'Uniform fixture requires exact float32 [ticks,columns]')
require(np.all(np.isfinite(uniforms)) and np.all((uniforms >= 0) & (uniforms < 1)) and
np.all(np.isfinite(probability)) and np.all((probability >= 0) & (probability <= 1)), 'Invalid uniform/probability fixture')
return uniforms < probability[None, :]
def requests_from_forced(forced, targets, *, cs_start_tick, cs_stop_tick):
"""Sort resulting requests only; never reorder the pre-sampling columns."""
_targets(targets)
require(isinstance(forced, np.ndarray) and forced.dtype == np.bool_ and forced.ndim == 2 and
forced.shape[1] == len(targets), 'Forced samples must be Boolean [steps,original PN columns]')
steps = forced.shape[0]; _clock(steps, cs_start_tick, cs_stop_tick)
ticks, columns = np.nonzero(forced[cs_start_tick:cs_stop_tick])
ticks = ticks.astype(np.int32) + np.int32(cs_start_tick)
neurons = targets[columns].copy()
order = np.lexsort((neurons, ticks))
requests = {'tick': ticks[order], 'neuron_idx': neurons[order]}
validate_requests(requests, steps)
for value in requests.values(): value.setflags(write=False)
generated = int(np.count_nonzero(forced)); retained = len(ticks)
return dict(requests=requests, audit=dict(steps=steps, CS_window=[cs_start_tick, cs_stop_tick],
full_matrix_boolean_hits=generated, retained_CS_requests=retained,
outside_CS_boolean_hits_masked=generated - retained,
original_PN_column_order_preserved=True, sorted_only_after_sampling=True,
duplicate_requests_removed=0, probabilities_clipped=False,
returned_requests_read_only=True, consumer_must_recheck_array_hashes=True,
forced_identity=array_identity(forced), targets_identity=array_identity(targets),
requests_identity={k: array_identity(v) for k, v in requests.items()}))
def _load_torch():
import torch
return torch
def generate_cs_requests(targets, rates_hz, *, seed, steps, cs_start_tick, cs_stop_tick,
device, max_uniform_draws, dt_ms=0.1):
"""Future explicit CUDA input generation; no implicit CPU fallback.
``seed`` is the caller's full semantic seed (Python int in [0,2**63)).
The generator is private to this call. No offset/modulo/truncation or
global/manual brain seed is used. Aliases must reuse the same complete
sampling descriptor, including steps/column order/window, not seed alone.
The caller owns authorization, resource admission, and external rate pins.
``device`` and ``max_uniform_draws`` must be explicitly supplied. The
latter bounds steps * PN columns, not total process/GPU memory. Retaining
initial_seed proves only integer delivery, not CUDA high-bit influence.
"""
require(type(seed) is int and 0 <= seed < 2**63, 'Exact full 63-bit Python integer seed required')
_clock(steps, cs_start_tick, cs_stop_tick)
probability = probabilities_f32(targets, rates_hz, dt_ms=dt_ms)
require(type(device) is str and re.fullmatch('cuda:[0-9]+', device), 'Explicit indexed CUDA device required; no CPU fallback')
require(type(max_uniform_draws) is int and max_uniform_draws > 0, 'Explicit positive integer uniform-draw budget required')
uniform_draws = steps * len(targets)
require(uniform_draws <= max_uniform_draws, 'Full matrix exceeds caller uniform-draw budget; no chunking or clipping')
torch = _load_torch()
require(torch.cuda.is_available() and torch.get_default_dtype() == torch.float32, 'Original CUDA/default-float32 sampler environment required')
actual_device = torch.device(device)
generator = torch.Generator(device=actual_device)
generator.manual_seed(seed)
require(generator.initial_seed() == seed, 'Generator did not retain the complete semantic seed')
p_gpu = torch.as_tensor(probability, dtype=torch.float32, device=actual_device)
# Exactly one complete draw. No chunking, per-PN loop, or tail-dependent redraw.
uniforms = torch.rand((steps, len(targets)), generator=generator, device=actual_device)
require(uniforms.dtype == torch.float32 and str(uniforms.device) == str(actual_device), 'Sampler dtype/device changed')
forced = (uniforms < p_gpu[None, :]).cpu().numpy()
result = requests_from_forced(forced, targets, cs_start_tick=cs_start_tick, cs_stop_tick=cs_stop_tick)
# Optional identifiers are recorded only when the installed Torch exposes
# them. An indexed device name is not presented as a UUID or external pin.
identity = {'uuid': None, 'pci_bus_id': None, 'query_error': None}
try:
properties = torch.cuda.get_device_properties(actual_device)
for key in ('uuid', 'pci_bus_id'):
value = getattr(properties, key, None)
if value is not None: identity[key] = str(value)
except (AttributeError, RuntimeError) as exc:
identity['query_error'] = type(exc).__name__ + ': ' + str(exc)
result['audit'].update(schema='phase4an-conditioning-input-v1-unregistered',
seed=seed, seed_bits=63, seed_hex=f'{seed:016x}', generator_initial_seed=generator.initial_seed(),
seed_conversion='exact integer; no truncation/modulo/offset', private_generator=True,
sampler='Torch CUDA rand default float32; strict U < float32 probability',
generator_type=type(generator).__module__ + '.' + type(generator).__qualname__,
numpy_version=str(np.__version__), torch_version=str(torch.__version__), torch_cuda_version=torch.version.cuda,
execution_device=str(uniforms.device), device_name=str(torch.cuda.get_device_name(actual_device)),
device_capability=list(torch.cuda.get_device_capability(actual_device)),
device_identity=identity, device_identity_verified_against_external_pin=False,
matrix_draw_calls=1, uniform_matrix_shape=[steps, len(targets)], uniform_scalar_draws=uniform_draws,
max_uniform_draws=max_uniform_draws, uniform_matrix_bytes=uniform_draws * np.dtype(np.float32).itemsize,
budget_scope='Uniform scalar count only, not total process or GPU memory',
sampled_before_window_mask=True, dt_ms=dt_ms, dt_float32_identity=array_identity(np.asarray([dt_ms], np.float32)),
rate_units='Hz', probability_identity=array_identity(probability), rates_identity=array_identity(rates_hz),
input_mapping_authority_verified_by_this_module=False,
reference_L_source_sha256=L_SOURCE_SHA, reference_AK_encoder_source_sha256=AK_ENCODER_SHA,
full_old_L_input_identity_claimed=False, CUDA_sequence_equivalence_tested_here=False,
CUDA_high_seed_bit_sequence_influence_tested_here=False,
timing_scope='Full epoch matrix drawn once, then explicit CS window; old L supplied PN requests over its full duration',
RNG_prefix_invariance_across_shapes_claimed=False, continuous_time_Poisson_claimed=False,
brain_reset_called=False, brain_executed=False, training_executed=False, protocol_registered=False)
return result
def validate_requests(requests, steps):
_clock(steps, 0, steps)
require(type(requests) is dict and set(requests) == {'tick', 'neuron_idx'}, 'Exact sparse request schema required')
for key, value in requests.items(): _vector(value, np.int32, key)
tick, neuron = requests['tick'], requests['neuron_idx']
require(tick.shape == neuron.shape and np.all((tick >= 0) & (tick < steps)) and
np.all((neuron >= 0) & (neuron < N_NEURONS)), 'Requests outside local clock/global neuron range')
keys = tick.astype(np.int64) * N_NEURONS + neuron
require(len(keys) < 2 or np.all(keys[1:] > keys[:-1]), 'Requests must already be sorted unique; no silent sorting/deduplication')
return keys
class RequestConflictError(ValueError):
def __init__(self, audit):
super().__init__('CS/US request overlap rejected; explicit boolean_union is required to combine it')
self.audit = audit
def merge_requests(cs, us, *, steps, conflict_policy='error'):
"""Compose two valid streams, recording every cross-source collision.
boolean_union explicitly reproduces a single Boolean request per neuron /
tick, not two deliveries. Separate source attempts are retained in audit
and uint8 source_mask (CS=1, US=2, overlap=3). This is not refractory
acceptance, actual spiking, an artificial D value, or an update rule.
"""
require(conflict_policy in ('error', 'boolean_union'), 'Unknown explicit conflict policy')
cs_keys, us_keys = validate_requests(cs, steps), validate_requests(us, steps)
overlap = np.intersect1d(cs_keys, us_keys, assume_unique=True)
conflicts = [dict(tick=int(k // N_NEURONS), neuron_idx=int(k % N_NEURONS), CS_attempts=1, US_attempts=1,
combined_boolean_requests=1 if conflict_policy == 'boolean_union' else None) for k in overlap]
audit = dict(schema='phase4an-request-composition-v1-unregistered', policy=conflict_policy,
CS_attempts=len(cs_keys), US_attempts=len(us_keys), cross_source_conflict_count=len(overlap), conflicts=conflicts,
duplicate_within_source_allowed=False, refractory_acceptance_evaluated=False, actual_spikes_evaluated=False,
input_identity={name: {k: array_identity(v) for k, v in values.items()} for name, values in (('CS', cs), ('US', us))})
if len(overlap) and conflict_policy == 'error': raise RequestConflictError(audit)
keys = np.union1d(cs_keys, us_keys)
sources = np.zeros(len(keys), np.uint8)
sources[np.searchsorted(keys, cs_keys)] |= np.uint8(1)
sources[np.searchsorted(keys, us_keys)] |= np.uint8(2)
requests = {'tick': (keys // N_NEURONS).astype(np.int32), 'neuron_idx': (keys % N_NEURONS).astype(np.int32)}
validate_requests(requests, steps)
for value in (*requests.values(), sources): value.setflags(write=False)
audit.update(output_requests=len(keys), explicit_boolean_overlap_collapses=len(overlap),
requests_identity={k: array_identity(v) for k, v in requests.items()}, source_mask_identity=array_identity(sources),
returned_arrays_read_only=True, consumer_must_recheck_array_hashes=True,
learning_or_D_command_defined=False, brain_executed=False, protocol_registered=False)
return dict(requests=requests, source_mask=sources, audit=audit)
@@ -0,0 +1,205 @@
"""Pure epoch-END adapter to the frozen AM/J eligibility implementation.
Only the two SHA-pinned source files are read. No event files, brain state,
weights, base arrays or artifacts are written. The caller supplies actual sparse
events and externally authenticated COMPLETE KC/mask mappings. Hashing supplied
arrays identifies their bytes; it does not establish anatomical completeness.
The returned overlay is NOT installed. Scheduled D is not actual DAN delivery.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
import math
from pathlib import Path
import sys
from types import ModuleType
import numpy as np
SCHEMA = "phase4an-conditioning-update-draft-v1"
BRIDGE_SHA256 = "174fb8d14619189e74b095e77a6db18b14c93046dc431c0b55710a5aa2dd2e61"
PHASE4J_SHA256 = "abb4574da85f00bd9ef61024c65993ef81d42651ff8d8d63c15dd69c6222471e"
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 _array(value, dtype, name):
_require(isinstance(value, np.ndarray) and value.ndim == 1 and value.dtype == np.dtype(dtype),
name + " requires an exact one-dimensional " + str(np.dtype(dtype)) + " array")
return value
def _evidence(array):
"""Original dtype and logical C-order raw bytes; no cast or rounding."""
return dict(dtype=array.dtype.str, shape=list(array.shape), nbytes=int(array.nbytes),
raw_c_order_sha256=hashlib.sha256(array.tobytes(order="C")).hexdigest())
def _owned(array):
result = np.array(array, copy=True, order="C")
result.setflags(write=False)
return result
def _pinned_bytes(path, expected, label):
raw = Path(path).read_bytes()
_require(hashlib.sha256(raw).hexdigest() == expected, label + " source SHA256 mismatch")
return raw
def _load_bridge(path):
raw = _pinned_bytes(path, BRIDGE_SHA256, "AM eligibility bridge")
# Execute precisely the verified bytes, not a second path-based import.
# Temporary isolated module registration supports the frozen dataclass;
# restore any prior module, rather than colliding with old globals.
name = "_conditioning_update_bridge_" + BRIDGE_SHA256
module = ModuleType(name)
module.__file__ = str(path)
previous = sys.modules.get(name)
sys.modules[name] = module
try:
exec(compile(raw, str(path), "exec"), module.__dict__)
finally:
if previous is None:
del sys.modules[name]
else:
sys.modules[name] = previous
_require(module.PHASE4J_SHA256 == PHASE4J_SHA256, "Frozen bridge/J pin disagreement")
return module
@dataclass(frozen=True)
class EpochUpdate:
delta_after: np.ndarray
sampled_trace: np.ndarray
final_trace: np.ndarray
receipt: dict
def update_epoch(*, step, neuron_idx, kc, candidate_edge_id, candidate_sources,
candidate_targets, candidate_compartments, delta_before, base_candidate,
steps, n_neurons, compartment, us_tick, d_command, frozen,
bridge_source, phase4j_source, trace_backend):
"""Return the epoch-boundary update; never apply it to the running brain.
trace_backend is mandatory: ``torch`` executes the frozen Torch CPU trace
expression; ``numpy`` remains its candidate compatibility implementation,
without a claim of per-call Torch equivalence. One declared US or no US is
supported. A US-less epoch requires D=0. Actual DAN delivery is deliberately
absent from this API and must be bound as separate coordinator evidence.
"""
_require(type(steps) is int and steps > 0, "steps must be a positive integer")
_require(type(n_neurons) is int and n_neurons > 0, "n_neurons must be a positive integer")
_require(us_tick is None or (type(us_tick) is int and 0 <= us_tick < steps), "us_tick must be None or an integer inside the epoch")
_require(type(d_command) in (int, float) and math.isfinite(d_command) and d_command in (0, 1),
"D must be the explicit scheduled scalar 0 or 1, not Boolean or measured DAN")
_require(us_tick is not None or d_command == 0, "No declared US tick requires D=0")
_require(type(frozen) is bool, "frozen must be Boolean")
_require(trace_backend in ("numpy", "torch"), "Explicit trace_backend must be numpy or torch")
arrays = dict(step=_array(step, "int32", "step"), neuron_idx=_array(neuron_idx, "int32", "neuron_idx"),
kc=_array(kc, "int32", "kc"), candidate_edge_id=_array(candidate_edge_id, "int64", "candidate_edge_id"),
candidate_sources=_array(candidate_sources, "int32", "candidate_sources"),
candidate_targets=_array(candidate_targets, "int32", "candidate_targets"),
delta_before=_array(delta_before, "float32", "delta_before"),
base_candidate=_array(base_candidate, "float32", "base_candidate"))
_require(isinstance(candidate_compartments, np.ndarray) and candidate_compartments.ndim == 1 and
candidate_compartments.dtype.kind == "U", "candidate_compartments must be a Unicode vector")
arrays["candidate_compartments"] = candidate_compartments
size = len(candidate_edge_id)
_require(size > 0 and np.all(candidate_edge_id >= 0) and np.all(candidate_edge_id[1:] > candidate_edge_id[:-1]),
"Candidate edge IDs must be nonempty, canonical sorted unique int64 IDs")
_require(all(len(arrays[name]) == size for name in ("candidate_sources", "candidate_targets",
"candidate_compartments", "delta_before", "base_candidate")), "Candidate mapping columns differ in length")
_require(np.all((candidate_targets >= 0) & (candidate_targets < n_neurons)), "Candidate target outside supplied graph size")
before_evidence = {name: _evidence(value) for name, value in arrays.items()}
# Every computation receives owned copies. Caller views/strides are accepted
# without changing their values or flags; dtype is never silently repaired.
work = {name: np.array(value, copy=True, order="C") for name, value in arrays.items()}
bridge = _load_bridge(bridge_source)
_pinned_bytes(phase4j_source, PHASE4J_SHA256, "Phase 4J")
trace = bridge.trace_from_events(work["step"], work["neuron_idx"], work["kc"], steps,
[] if us_tick is None else [us_tick], phase4j_source=phase4j_source,
backend=trace_backend, n_neurons=n_neurons)
sampled = trace.last_command_trace
after, original = bridge.apply_eligibility(sampled, work["kc"], work["candidate_sources"],
work["candidate_compartments"], compartment, work["delta_before"], work["base_candidate"],
d=d_command, frozen=frozen, phase4j_source=phase4j_source, n_neurons=n_neurons)
# Diagnostics only; the update is exclusively the frozen apply_v2 above.
selected = work["candidate_compartments"] == compartment
local = np.searchsorted(work["kc"], work["candidate_sources"])
eligible = (sampled[local] > 0) & selected
changed = after.view(np.uint32) != work["delta_before"].view(np.uint32)
value_changed = after != work["delta_before"]
floor = -bridge.FLOOR_FRACTION * work["base_candidate"]
floor_before = work["delta_before"] == floor
floor_after = after == floor
_require(bridge.bit_equal(after[~selected], work["delta_before"][~selected]), "Off-compartment identity failed")
if frozen or d_command == 0:
_require(bridge.bit_equal(after, work["delta_before"]), "Zero-D/frozen identity failed")
original_report = original["original_report"]
attempted = 0 if original_report is None else original_report["updated_edges"]
if original_report is not None:
_require(original_report["eligible_edges"] == int(eligible.sum()), "Frozen eligible-edge report disagrees with its sampled trace")
delta_after, sampled_trace, final_trace = (_owned(a) for a in (after, sampled, trace.final))
_require(all(_evidence(value) == before_evidence[name] for name, value in arrays.items()), "Caller input bytes changed")
_pinned_bytes(bridge_source, BRIDGE_SHA256, "AM eligibility bridge")
_pinned_bytes(phase4j_source, PHASE4J_SHA256, "Phase 4J")
receipt = dict(schema=SCHEMA, status="DRAFT-COMPUTED-EPOCH-UPDATE-NOT-INSTALLED",
source_pins=dict(eligibility_bridge_sha256=BRIDGE_SHA256, phase4j_sha256=PHASE4J_SHA256),
input_arrays=before_evidence,
command=dict(steps=steps, n_neurons=n_neurons, compartment=compartment, us_tick=us_tick,
D_scheduled=float(d_command), frozen=frozen, trace_backend=trace_backend),
semantics=dict(application="epoch_boundary_after_all_recorded_ticks; affects subsequent epoch only",
sampling="after_current_tick_spike", trace_initial_state="zero at epoch boundary",
no_US_sample="canonical float32 +0, not final trace", D_definition="artificial scheduled scalar",
actual_DAN_delivery_observed=False, actual_DAN_delivery_used_to_gate=False,
actual_DAN_evidence_authority="separate externally bound epoch record, not an input to this function",
original_base_units="mV", delta_units="mV", delta_dtype="float32",
candidate_target_role="identity evidence only; update uses original source/compartment mapping"),
fixed_rule=dict(dt_ms=bridge.DT_MS, trace_tau_ms=bridge.TAU_MS, trace_cap=bridge.TRACE_CAP,
eta_mv=bridge.ETA_MV, polarity=bridge.POLARITY, floor_fraction=bridge.FLOOR_FRACTION),
trace_audit=trace.audit, apply_audit=original,
counts=dict(events=len(step), actual_KC_events=trace.audit["actual_kc_events"],
candidates=size, selected_compartment_edges=int(selected.sum()), sampled_eligible_selected_edges=int(eligible.sum()),
original_updated_edges_attempted=int(attempted), original_apply_v2_executed=original["original_apply_v2_executed"],
actual_value_changed_edges=int(value_changed.sum()), actual_bitwise_changed_edges=int(changed.sum()),
actual_off_compartment_bitwise_changed_edges=int(changed[~selected].sum()),
selected_at_floor_before=int((selected & floor_before).sum()),
selected_at_floor_after=int((selected & floor_after).sum()),
selected_newly_at_floor=int((selected & floor_after & ~floor_before).sum()),
sampled_eligible_selected_already_at_floor=int((eligible & floor_before).sum())),
count_limitations="original updated_edges counts nonzero attempted increments, including saturation; floor counts do not prove why any edge was unchanged",
outputs={"delta_after": _evidence(delta_after), "sampled_trace": _evidence(sampled_trace), "final_trace": _evidence(final_trace)},
inputs_exact_unchanged=True, off_compartment_exact_unchanged=True,
numpy_torch_bitwise_comparison_performed=False,
frozen_Torch_expression_executed=trace_backend == "torch",
complete_KC_identity_verified=False, complete_mask_identity_verified=False, full_graph_authority_verified=False,
mapping_hash_scope="supplied logical array bytes only; coordinator must bind complete canonical KC and candidate mask",
brain_dynamics_executed=False, weights_installed=False, base_modified=False,
training_executed_by_this_function=False, scientific_learning_pass_claimed=False, candidate_generated=False)
receipt["payload_sha256"] = _sha(receipt)
return EpochUpdate(delta_after, sampled_trace, final_trace, receipt)
def verify_receipt(receipt, **epoch_inputs):
"""Recompute from supplied arrays/sources; never trust a self-hash alone."""
_require(type(receipt) is dict and "payload_sha256" in receipt, "Invalid epoch receipt")
_require(receipt["payload_sha256"] == _sha({k: v for k, v in receipt.items() if k != "payload_sha256"}),
"Epoch receipt payload SHA mismatch")
actual = update_epoch(**epoch_inputs)
_require(_canonical(receipt) == _canonical(actual.receipt), "Receipt differs from exact recomputation in this environment")
return True
@@ -0,0 +1,57 @@
{
"schema": "conditioning-preparation-cpu-test-results-v1",
"status": "PASS-SYNTHETIC-IMPLEMENTATION-TESTS-NOT-LEARNING",
"recorded_utc": "2026-09-30T09:05:52Z",
"source_inventory_sha256": "2c72d47d09b4ad1949dc6cfdc170f64847ff75f95ded5d20b421fb5a75e0eb84",
"source_files": 20,
"server": "192.168.10.51",
"environment": {
"frozen_AN_sources_unchanged_after_tests": 21,
"python": "3.10.12",
"numpy": "2.2.6",
"torch": "2.14.0+cu130",
"cuda_toolkit": "13.0"
},
"test_result": {
"tests": 119,
"failures": 0,
"errors": 0,
"skipped": 0,
"wall_seconds_reported_by_unittest": 241.205,
"exit_code": 0,
"modules": [
"test_seed_registry",
"test_trial_plan",
"test_biological_contract",
"test_metrics",
"test_evaluation_plan",
"test_evaluation_policy",
"test_conditioning_inputs",
"test_conditioning_update"
],
"gpu_visible": false,
"brain_runs": 0
},
"invocation_scope": "Eight explicitly named unittest modules; CUDA_VISIBLE_DEVICES empty; Python -B; original Phase4J source pinned.",
"observed_stdout_stderr_summary": "Ran 119 tests in 241.205s; OK",
"local_optional_torch_test_skip_resolved_on_server": true,
"real_CUDA_sampler_validated": false,
"real_CUDA_high_seed_bit_influence_validated": false,
"synthetic_numpy_torch_CPU_trace_comparison_executed": true,
"full_frozen_roots_rehashed_by_this_test_run": false,
"frozen_AN_registered_sources_checked_before": 21,
"frozen_AN_registered_sources_checked_after": 21,
"scientific_master_selected": false,
"real_inputs_generated": false,
"real_brain_runs": 0,
"conditioning_executed": false,
"weights_installed": false,
"learned_candidate_generated": false,
"remaining": [
"actual file/parent/state/delta authority coordinator",
"finite preregistered real CUDA input and two-epoch update installation check",
"explicit new-overlay plus retained-neural-state boundary for retention",
"reversal and retention functional gate implementation",
"measured full time/storage budget and scientific registration"
]
}
@@ -0,0 +1,122 @@
{
"schema": "conditioning-preparation-source-inventory-v1",
"status": "DRAFT-NOT-REGISTERED",
"engineering_commit": "aeccb7d0ccdcf6788e0a8de60e7ab96f70dbe734",
"model_directory": "models/male-cns-lif/olfactory-conditioning-lab-v1",
"files": [
{
"path": "EVIDENCE_DRAFT.md",
"bytes": 10788,
"sha256": "8455c12e3a6bb1b22a6e38f9017293fe5deea37ba3ec61e13598d00dafc12296"
},
{
"path": "PROTOCOL_DRAFT.md",
"bytes": 27118,
"sha256": "8ebecb1f3d012ddc29a1cb727269dd277629c53565e09978bf9dbf7140cb8508"
},
{
"path": "README_DRAFT.md",
"bytes": 7892,
"sha256": "50be97f64097b681355993a8afd7f467ea187e99e88b19ca30561e08d517999f"
},
{
"path": "EVALUATION_POLICY_DRAFT.md",
"bytes": 4024,
"sha256": "a84170a61564715a1532f20df4c6a330f0f9725c383db48b8f3e1a4b5fba0bc8"
},
{
"path": "biological_contract.py",
"bytes": 12374,
"sha256": "e09f0f2b5e6ec746176e3a1aa2fdee1a15385e14e96bd343d14379a759f3ae43"
},
{
"path": "test_biological_contract.py",
"bytes": 7729,
"sha256": "0ce796e6ad1a4cbcb9bcb760eff628a9562b4a809cc9c177a9a0d88df68f02b3"
},
{
"path": "metrics.py",
"bytes": 31945,
"sha256": "47319e21db3c8b030b1bafbf0703cd86fb588d23bb29436accbacdba73d08c3c"
},
{
"path": "test_metrics.py",
"bytes": 18230,
"sha256": "1928894d248dd37fad0fc67bc9dcfeac27c4d8be4aec22f54b9ef20a10ea8645"
},
{
"path": "seed_registry.py",
"bytes": 5651,
"sha256": "0da033b132095ec3087b5d9f64e53c2e391fa50ab0b7653b31296eeb84799b7e"
},
{
"path": "test_seed_registry.py",
"bytes": 5489,
"sha256": "7aca38f3c975e3ad4f6337d83b826c09afef336381d124c22ff0eb2eb60a0834"
},
{
"path": "trial_plan.py",
"bytes": 16606,
"sha256": "6fed3f494d075339a7e5b001d43b2598640b189e038952fbd4d41200bacac587"
},
{
"path": "test_trial_plan.py",
"bytes": 9651,
"sha256": "927678956ae9959a047b1cd258ee8dee22eb6842a96ff4b2ba2db077310163c9"
},
{
"path": "evaluation_plan.py",
"bytes": 16722,
"sha256": "991c813ff3322b230947f302b12b04ec382c3b632817f94b698101e4b17efa7d"
},
{
"path": "test_evaluation_plan.py",
"bytes": 10923,
"sha256": "21d2402025122d8665fb1f32e184fd8e30fc17242810fc70901b7f7fc8c53bfa"
},
{
"path": "evaluation_policy.py",
"bytes": 16826,
"sha256": "21331a9ff3567dc6f6399e16bb91ef486d74231f5db51799beb6dd1bf71d022b"
},
{
"path": "test_evaluation_policy.py",
"bytes": 15656,
"sha256": "421a31b76eaa547e890c291d2c38a415352d9ccad6fccc800461f522ff4c83f1"
},
{
"path": "conditioning_inputs.py",
"bytes": 13967,
"sha256": "19382e066e450140bb653a3661f980ffe7cc62f412020ef57df0b6e6b3cbb8ba"
},
{
"path": "test_conditioning_inputs.py",
"bytes": 16186,
"sha256": "95e37c770e78ce722a3007c5b371c51aec2989634cddfe5b414ac3fa817e5c22"
},
{
"path": "conditioning_update.py",
"bytes": 12222,
"sha256": "96efff678cff910654eb08e86f1a17789e4e5589e340345f419c7027a7d547e2"
},
{
"path": "test_conditioning_update.py",
"bytes": 10625,
"sha256": "1c25d93753932ace06abc876fe0867546b3bb330de3e602da6aec7bb4588e4bd"
}
],
"frozen_AN_sources_included": false,
"frozen_AN_inventory": "registry/phase4an-engineering-validation/source-identity.json",
"reference_pins": {
"AM_eligibility_bridge": "174fb8d14619189e74b095e77a6db18b14c93046dc431c0b55710a5aa2dd2e61",
"phase4J_source": "abb4574da85f00bd9ef61024c65993ef81d42651ff8d8d63c15dd69c6222471e",
"AK_encoder": "0061689abb1832e963718bd836bc8a50009ee38b258143a753b4d59e63414925"
},
"cpu_test_receipt": "draft-cpu-test-results.json",
"inventory_is_experiment_registration": false,
"model_directory_sealed": false,
"scientific_inputs_generated": false,
"conditioning_executed": false,
"learned_candidate_generated": false,
"notes": "Versioned preparation snapshot only. The future coordinator must bind actual complete data, seeds, inputs, state/delta lineage, resource budget and registered gates. Reference pins identify dependencies; this inventory alone does not rerun them or prove scientific validity."
}
@@ -0,0 +1,260 @@
"""Pure, UNREGISTERED evaluation inventory; no files, RNG draws or execution.
build_evaluation_plan(trial_payload, config) requires the complete existing
trial_plan payload, including its caller-chosen master and seed manifest. It
reuses that payload's 80 evaluation inputs; repeated probes are paired
observations, never extra independent training replicates. No defaults select
CS offset, reversal curve checkpoints, control snapshot boundaries, or baseline
execution policy. See required_choices(). A complete inventory is not approval,
an executed snapshot, an externally verified source pin, or a scientific gate.
"""
from __future__ import annotations
from collections import Counter
import hashlib
import json
import seed_registry
import trial_plan
SCHEMA = "phase4an-evaluation-plan-draft-v1"
CONFIG_SCHEMA = "phase4an-evaluation-config-draft-v1"
CONFIG_FIELDS = frozenset(("schema", "probe_steps", "cs_start_tick",
"cs_stop_tick_exclusive", "reversal_checkpoints", "baseline_execution",
"snapshot_binding_policy", "snapshot_bindings"))
BINDING_FIELDS = frozenset(("block_id", "assay", "arm", "checkpoint", "completed_epochs"))
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 required_choices():
"""Describe mandatory choices, without filling or approving any of them."""
return {
"probe_steps": "Explicit integer 1200: the draft's fixed 120 ms metric window.",
"cs_start_tick": "Explicit integer 0: stimulus onset defines probe latency.",
"cs_stop_tick_exclusive": "Explicit integer in [1,1200]; draft does not settle evaluation CS offset.",
"reversal_checkpoints": "Explicit sorted subset of [0,1,2,5,10,20,50], including 0 and 50.",
"baseline_execution": "Only execute_each_snapshot is implemented; declare it explicitly. No output deduplication.",
"snapshot_binding_policy": "Explicit completed_epochs; nominal control labels are not actual update counts.",
"snapshot_bindings": "Every block/assay/arm/checkpoint, in trial-plan order, with explicit completed_epochs. No control alignment is guessed.",
"authority": "Provide the complete existing trial payload; no master seed is chosen here. Source/input pins remain coordinator responsibilities.",
}
def _configuration(config):
_require(type(config) is dict, "Explicit evaluation config is required; see required_choices()")
_require(set(config) == CONFIG_FIELDS,
"Evaluation config fields: missing=" + str(sorted(CONFIG_FIELDS - set(config))) +
"; unknown=" + str(sorted(set(config) - CONFIG_FIELDS)))
_require(config["schema"] == CONFIG_SCHEMA, "Wrong evaluation config schema")
_require(type(config["probe_steps"]) is int and config["probe_steps"] == 1200,
"Explicit probe_steps must be integer 1200")
_require(type(config["cs_start_tick"]) is int and config["cs_start_tick"] == 0,
"Explicit CS onset must be integer tick 0")
stop = config["cs_stop_tick_exclusive"]
_require(type(stop) is int and 0 < stop <= 1200,
"Evaluation CS stop tick is required; no training-CS default is inferred")
checks = config["reversal_checkpoints"]
_require(type(checks) is list and len(checks) >= 2 and
all(type(v) is int and v in trial_plan.PROBE_CHECKPOINTS for v in checks) and
checks == sorted(set(checks)) and checks[0] == 0 and checks[-1] == 50,
"Explicit reversal checkpoints must include 0/50, sorted, unique, and within the draft curve")
_require(config["baseline_execution"] == "execute_each_snapshot",
"Declare execute_each_snapshot explicitly; baseline reuse is not implemented or inferred")
_require(config["snapshot_binding_policy"] == "explicit_completed_epochs",
"Explicit control snapshot epoch bindings are mandatory")
_require(type(config["snapshot_bindings"]) is list, "snapshot_bindings must be an explicit ordered list")
return json.loads(_canonical(config))
def _binding_inventory(plan, config):
expected, specs = [], []
for block in plan["blocks"]:
for suite in block["suites"]:
assay = suite["assay"]
points = config["reversal_checkpoints"] if assay == "reversal" else block["evaluation_checkpoints"]
for arm in suite["arms"]:
for point in points:
expected.append((block["block_id"], assay, arm["arm"], point))
specs.append((block, suite, arm))
bindings = config["snapshot_bindings"]
_require(len(bindings) == len(expected), "Missing/extra snapshot bindings: require every block, arm and checkpoint")
previous = {}
result = []
for binding, identity, spec in zip(bindings, expected, specs):
_require(type(binding) is dict and set(binding) == BINDING_FIELDS, "Invalid snapshot binding fields")
actual = tuple(binding[key] for key in ("block_id", "assay", "arm", "checkpoint"))
_require(all(type(v) is str for v in actual[:3]) and type(actual[3]) is int and actual == identity,
"Snapshot bindings must match exact canonical coverage/order/types")
block, suite, arm = spec
count = binding["completed_epochs"]
_require(type(count) is int and 0 <= count <= len(arm["epochs"]), "Invalid completed_epochs")
point = identity[-1]
_require((count == 0) == (point == 0), "Checkpoint zero is before the arm's first epoch")
_require(point != 50 or count == len(arm["epochs"]), "Terminal checkpoint must include the entire arm, including neutral/blank epochs")
key = identity[:3]
_require(key not in previous or count > previous[key], "Snapshot epochs must advance strictly")
previous[key] = count
us = sum(e["us"]["requested"] for e in arm["epochs"][:count])
updates = sum(e["update_enabled"] and e["D_command"] != 0 for e in arm["epochs"][:count])
if arm["arm"] in ("paired", "reversal", "no_reversal"):
_require(us == point and updates == point,
"Paired/reversal checkpoint must contain exactly its declared reinforcement opportunities")
result.append((binding, block, suite, arm, us, updates))
return result
def _streams(trial):
"""Reconstruct evaluation semantic keys, not just plausible seed strings."""
manifest = trial["seed_manifest"]
master = trial["plan"]["master"]
independent = {(r["split"], tuple(r["key"])): r for r in manifest["independent_streams"]}
non_eval = {r["seed"] for r in manifest["independent_streams"] if r["split"] != "evaluation"}
used, result = set(), {}
for block in trial["plan"]["blocks"]:
_require([v["cue"] for v in block["evaluation_streams"]] == ["A", "B"], "Both original evaluation cues are required")
for row in block["evaluation_streams"]:
cue = row["cue"]
key = (block["block_id"], "evaluation", "CS", cue, "00", "base-probe")
seed, derivation = seed_registry.seed_for(master, "evaluation", key)
registered = independent.get(("evaluation", key))
_require(registered == {"split": "evaluation", "key": list(key), "seed": seed,
"derivation_sha256": derivation}, "Missing exact evaluation registry key")
stream_id = "evaluation:" + _sha([trial_plan.SCHEMA, "stream-id", master, "evaluation", list(key)])
_require(type(row["seed"]) is int and row["seed"] == seed and row["seed_bits"] == 63 and
row["stream_id"] == stream_id and row["odor"] == trial_plan.ODORS[cue] and row["role"] == "CS",
"Evaluation seed/key/odor lineage mismatch")
_require(seed not in used and seed not in non_eval, "Evaluation seed overlaps another independent stream")
used.add(seed)
result[(block["block_id"], cue)] = dict(stream_id=stream_id, split="evaluation",
semantic_key=list(key), seed=seed, seed_bits=63, derivation_sha256=derivation,
input_id=block["block_id"] + "/evaluation/input-" + cue,
reuse="paired repeated probes within this training replicate; not additional independent seeds")
_require(len(result) == manifest["split_counts"]["evaluation"] == 80, "Exactly the existing 80 evaluation streams are required")
return result
def _snapshot_id(block_id, assay, arm, checkpoint):
return f"{block_id}/{assay}/{arm}/checkpoint-{checkpoint:02d}"
def _assemble(trial, config):
"""Internal assembly; public entry points first validate full trial authority."""
config = _configuration(config)
records = _binding_inventory(trial["plan"], config)
streams = _streams(trial)
snapshots, probes = [], []
def emit(block, snapshot):
common = dict(block_id=block["block_id"], mode=block["mode"], dan_type=block["dan_type"],
compartment_verbatim=block["compartment_verbatim"], training_replicate=block["replicate"],
original_reinforced_cue=block["initial_reinforced_cue"], original_neutral_cue=block["initial_neutral_cue"],
original_S_orientation="original reinforced minus original neutral, never flipped for reversal")
snapshot = dict(common, **snapshot)
snapshots.append(snapshot)
for cue in trial_plan.CUES:
probes.append(dict(common, probe_id=snapshot["snapshot_id"] + "/probe-" + cue,
snapshot_id=snapshot["snapshot_id"], baseline_snapshot_id=snapshot["baseline_snapshot_id"],
assay=snapshot["assay"], arm=snapshot["arm"], checkpoint=snapshot["checkpoint"],
elapsed_idle_ticks=snapshot["elapsed_idle_ticks"], cue=cue, odor=trial_plan.ODORS[cue],
steps=config["probe_steps"], dt_ms=0.1, cs_start_tick=config["cs_start_tick"],
cs_stop_tick_exclusive=config["cs_stop_tick_exclusive"],
input=streams[(block["block_id"], cue)], execution="independent fork; discard probe terminal state",
US_requested=False, D_command=0.0, weight_updates_enabled=False,
feedback_to_training_or_idle=False, adds_training_replicate=False))
for binding, block, suite, arm, us, updates in records:
block_id, assay, name = block["block_id"], suite["assay"], arm["arm"]
point, count = binding["checkpoint"], binding["completed_epochs"]
last = None if count == 0 else arm["epochs"][count - 1]["epoch_id"]
delta_source = (dict(arm["delta_start"]) if count == 0 else
{"kind": "epoch_delta_after", "source": last + "/delta-after"})
baseline = _snapshot_id(block_id, "normal" if assay == "reversal" else assay,
"paired" if assay == "reversal" else name, 0)
emit(block, dict(snapshot_id=_snapshot_id(block_id, assay, name, point), assay=assay, arm=name,
checkpoint=point, checkpoint_semantics="nominal reinforcement progress; control epoch boundary explicitly supplied",
completed_epochs=count, last_completed_epoch_id=last,
actual_US_requests_completed=us, enabled_nonzero_D_update_opportunities_completed=updates,
arm_reinforced_cue=arm["reinforced_cue"], delta_source=delta_source,
neural_state_source={"kind": "common_frozen_reset", "from_training_terminal_state": False},
baseline_snapshot_id=baseline, elapsed_idle_ticks=None,
probe_count=2, snapshot_mutated_by_planning=False))
for block in trial["plan"]["blocks"]:
block_id = block["block_id"]
normal = {a["arm"]: a for a in block["suites"][0]["arms"]}
for branch in block["retention"]["branches"]:
name = branch.split("/")[1]
terminal_epoch = normal[name]["epochs"][-1]["epoch_id"]
lineage = block_id + "/retention/" + name + "/idle-10s"
for seconds in block["retention"]["checkpoint_seconds"]:
ticks = seconds * 10000
emit(block, dict(snapshot_id=f"{block_id}/retention/{name}/idle-{seconds:02d}s",
assay="retention", arm=name, checkpoint=None, completed_epochs=len(normal[name]["epochs"]),
last_completed_epoch_id=terminal_epoch, delta_source={"kind": "epoch_delta_after", "source": terminal_epoch + "/delta-after"},
neural_state_source={"kind": "actual_terminal_checkpoint" if seconds == 0 else "continuous_idle_checkpoint",
"terminal_epoch_id": terminal_epoch, "idle_lineage_id": lineage,
"elapsed_ticks": ticks, "total_idle_ticks": 100000,
"one_second_is_prefix_of_same_ten_second_idle": True,
"carry": ["v", "g", "ref", "spike_ring", "apl_state", "apl_pending", "absolute_tick", "delta", "relevant_rng_state"],
"reset_or_analytic_time_advance": False},
baseline_snapshot_id=_snapshot_id(block_id, "normal", name, 0),
elapsed_idle_ticks=ticks, probe_count=2, snapshot_mutated_by_planning=False))
ids = {row["snapshot_id"] for row in snapshots}
_require(len(ids) == len(snapshots) and all(p["baseline_snapshot_id"] in ids for p in probes),
"Duplicate snapshot or missing original pre baseline")
by_assay = Counter(p["assay"] for p in probes)
steps = len(probes) * config["probe_steps"]
result = dict(schema=SCHEMA, status="DRAFT-NOT-REGISTERED", config=config,
trial_payload_sha256=trial["payload_sha256"], seed_manifest_sha256=trial["seed_manifest"]["payload_sha256"],
config_sha256=_sha(config), snapshots=snapshots, probes=probes,
budget=dict(snapshot_count=len(snapshots), evaluation_probes=len(probes),
probes_by_assay=dict(sorted(by_assay.items())), evaluation_steps=steps,
evaluation_biological_seconds=steps / 10000,
preexisting_training_control_reversal_epochs=trial["budget"]["training_control_reversal_epochs"],
preexisting_minimum_steps_excluding_evaluation=trial["budget"]["minimum_steps_excluding_evaluation"],
planned_scientific_steps_including_evaluation=trial["budget"]["minimum_steps_excluding_evaluation"] + steps,
planned_scientific_biological_seconds=(trial["budget"]["minimum_steps_excluding_evaluation"] + steps) / 10000,
independent_evaluation_inputs=80, independent_training_blocks=40,
evaluation_probes_are_training_replicates=False, baseline_output_deduplication=False,
scope="conditional full draft inventory, not an executed count or wall-time/storage estimate",
excludes=["implementation validation", "frozen regressions", "encoding", "I/O", "resource reserve"]),
evaluation_inventory_structurally_complete=True, scientific_gate_added=False,
protocol_approval_claimed=False, execution_authorized=False, training_executed=False,
inputs_generated=False, input_generator_implementation_verified=False, candidate_generated=False,
learning_pass_claimed=False, external_source_and_snapshot_authority_verified=False,
subsequent_assays_condition="not authorized here; full planned inventory remains subject to the draft's acquisition gate")
result["payload_sha256"] = _sha(result)
# No returned nested object aliases caller-owned trial/config structures.
return json.loads(_canonical(result))
def build_evaluation_plan(trial_payload, config):
"""Build a complete draft inventory, with mandatory explicit choices."""
_configuration(config) # Report missing choices before expensive reconstruction.
trial_plan.validate_trial_plan(trial_payload)
return _assemble(trial_payload, config)
def validate_evaluation_plan(payload, trial_payload, config):
"""Reconstruct every probe/binding; an updated self-hash cannot excuse drift."""
_require(type(payload) is dict and "payload_sha256" in payload, "Invalid evaluation payload")
_require(payload["payload_sha256"] == _sha({k: v for k, v in payload.items() if k != "payload_sha256"}),
"Evaluation payload SHA mismatch")
expected = build_evaluation_plan(trial_payload, config)
_require(_canonical(payload) == _canonical(expected), "Evaluation coverage, binding, type, seed, or order differs from exact reconstruction")
return True
@@ -0,0 +1,294 @@
"""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
@@ -0,0 +1,494 @@
"""Pure Phase 4AN proposal metrics; no file I/O, simulation or fitted decoder.
The caller must bind complete anatomical population membership (not a plastic
mask subset), finalized execution receipts, input identities and seed lineages
to its preregistration. These functions validate arrays/declared coverage; they
cannot establish provenance by trusting a caller's Boolean or SHA string.
All gates below are NEW DRAFT hypotheses, not a historical learning result.
"""
from __future__ import annotations
import hashlib
import json
import math
from collections.abc import Mapping
import numpy as np
SCHEMA = 'phase4an-metrics-draft-v1'
CUES = ('4-ethylphenol', '3-hexanone')
MODES = ('physiological', 'matched_energy')
DAN_TYPES = ('PAM15', 'PPL103')
CONTROLS = ('D=0', 'frozen', 'random reward', 'unpaired', 'CS-only', 'DAN-only')
ARMS = ('paired',) + CONTROLS
POPULATIONS = ('MBON', 'legacyDownstream', 'nonMBONDownstream', 'MBON01', 'MBON12')
SEPARATION_POPULATIONS = ('MBON', 'legacyDownstream', 'nonMBONDownstream')
POPULATION_SEMANTICS = {
'MBON': 'complete caller-bound anatomical MBON population',
'legacyDownstream': 'complete frozen H downstream membership, including MBONs; never mask-filtered',
'nonMBONDownstream': 'exact legacyDownstream minus MBON; NEW hypothesis readout, not the old H population',
'MBON01': 'complete caller-bound MBON01 type, not plastic-mask targets only',
'MBON12': 'complete caller-bound MBON12 type, not plastic-mask targets only',
}
STATE_KEYS = frozenset(('step', 'neuron_idx', 'counts', 'v', 'g', 'ref'))
def require(condition, message):
if not condition:
raise ValueError(message)
def _hash(value):
return hashlib.sha256(np.ascontiguousarray(value, dtype=value.dtype.newbyteorder('<')).tobytes()).hexdigest()
def _json_hash(value):
raw = json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode('utf-8')
return hashlib.sha256(raw).hexdigest()
def _sha(value):
return type(value) is str and len(value) == 64 and all(c in '0123456789abcdef' for c in value)
def _token(value, name):
require(type(value) is str and bool(value) and value.strip() == value, 'Invalid ' + name)
return value
def _vector(value, dtype, name, size=None):
require(isinstance(value, np.ndarray) and value.dtype == np.dtype(dtype) and value.ndim == 1,
name + ' requires an exact ' + dtype + ' vector')
require(size is None or value.shape == (size,), name + ' shape mismatch')
return value
def _ids(value, name):
_vector(value, 'int32', name)
require(len(value) > 0 and np.all((value >= 0) & (value < 166700)) and np.all(value[1:] > value[:-1]),
name + ' must be nonempty sorted unique global neuron indices')
return value
def _positions(nodes, values, name):
position = np.searchsorted(nodes, values)
require(np.all(position < len(nodes)), name + ' contains absent neurons')
require(np.array_equal(nodes[position], values), name + ' contains absent neurons')
return position
def validate_populations(nodes, populations):
"""Validate full caller-bound type lists; never derive them from an edge mask."""
_ids(nodes, 'nodes')
require(isinstance(populations, Mapping) and set(populations) == set(POPULATIONS),
'Require exactly MBON/legacyDownstream/nonMBONDownstream/MBON01/MBON12 memberships')
for name in POPULATIONS:
_positions(nodes, _ids(populations[name], name), name)
for name in ('MBON01', 'MBON12'):
_positions(populations['MBON'], populations[name], name + ' outside MBON')
require(not len(np.intersect1d(populations['MBON01'], populations['MBON12'])), 'Type populations overlap')
_positions(populations['legacyDownstream'], populations['MBON'], 'MBON outside legacyDownstream')
require(np.array_equal(populations['nonMBONDownstream'],
np.setdiff1d(populations['legacyDownstream'], populations['MBON'])),
'nonMBONDownstream must equal the complete legacyDownstream minus MBON')
payload = {name: populations[name].tolist() for name in POPULATIONS}
return dict(indices=payload, membership_sha256=_json_hash(payload),
population_semantics=dict(POPULATION_SEMANTICS),
complete_anatomical_membership_authority='caller preregistration; never inferred from mask')
def validate_state(state, *, nodes, steps, dt_ms=.1):
"""Check every original state element and all sparse events without mutation."""
_ids(nodes, 'nodes')
require(type(steps) is int and 0 < steps <= np.iinfo(np.int32).max, 'Invalid steps')
require(type(dt_ms) in (int, float) and math.isfinite(dt_ms) and dt_ms > 0, 'Invalid dt_ms')
require(isinstance(state, Mapping) and set(state) == STATE_KEYS, 'State must contain exactly six runtime keys')
n = len(nodes)
for key, dtype in (('counts', 'int32'), ('ref', 'int32'), ('v', 'float64'), ('g', 'float64')):
_vector(state[key], dtype, key, n)
require(np.isfinite(state[key]).all(), 'Nonfinite state: ' + key)
require(np.all((state['counts'] >= 0) & (state['counts'] <= steps)), 'Invalid counts')
require(np.all((state['ref'] >= 0) & (state['ref'] <= 22)), 'Invalid refractory state')
ticks = _vector(state['step'], 'int32', 'step')
events = _vector(state['neuron_idx'], 'int32', 'neuron_idx', len(ticks))
require(np.all((ticks >= 0) & (ticks < steps)), 'Event tick outside trial')
require(len(ticks) < 2 or np.all((ticks[1:] > ticks[:-1]) |
((ticks[1:] == ticks[:-1]) & (events[1:] > events[:-1]))),
'Events must be sorted unique by tick/global neuron')
local = _positions(nodes, events, 'events')
require(np.array_equal(np.bincount(local, minlength=n), state['counts']), 'Sparse events/counts mismatch')
first = np.full(n, -1, dtype=np.int32)
if len(ticks):
first.fill(steps)
np.minimum.at(first, local, ticks)
first[first == steps] = -1
return dict(first_tick=first, local_events=local,
hashes={key: _hash(state[key]) for key in sorted(STATE_KEYS)})
def _first_ms(first, dt_ms):
present = first[first >= 0]
return float(int(present.min()) * dt_ms) if len(present) else None
def _latencies(first, dt_ms):
return [None if int(tick) < 0 else float(int(tick) * dt_ms) for tick in first]
def _population_change(pre, post, checked_pre, checked_post, nodes, local, dt_ms, vectors):
cp, cq = pre['counts'][local].astype(np.int64), post['counts'][local].astype(np.int64)
fp, fq = checked_pre['first_tick'][local], checked_post['first_tick'][local]
lp, lq = _first_ms(fp, dt_ms), _first_ms(fq, dt_ms)
both = (fp >= 0) & (fq >= 0)
ep = np.isin(checked_pre['local_events'], local)
eq = np.isin(checked_post['local_events'], local)
events_same = (np.array_equal(pre['step'][ep], post['step'][eq]) and
np.array_equal(pre['neuron_idx'][ep], post['neuron_idx'][eq]))
try:
with np.errstate(over='raise', invalid='raise'):
dv, dg = post['v'][local] - pre['v'][local], post['g'][local] - pre['g'][local]
vmax, gmax = float(np.max(np.abs(dv))), float(np.max(np.abs(dg)))
except FloatingPointError as error:
raise ValueError('Nonfinite derived final-state difference') from error
result = dict(neurons=len(local), pre_total_spikes=int(cp.sum()), post_total_spikes=int(cq.sum()),
spike_count_l1=int(np.abs(cq - cp).sum()), spike_count_changed_neurons=int(np.count_nonzero(cp != cq)),
events_changed=not events_same, pre_first_latency_ms=lp, post_first_latency_ms=lq,
first_latency_delta_ms=None if lp is None or lq is None else float(lq - lp),
defined_first_latency_changed=bool(lp is not None and lq is not None and lp != lq),
first_latency_both_defined_neurons=int(both.sum()),
first_latency_changed_among_both_defined_neurons=int(np.count_nonzero(both & (fp != fq))),
first_latency_became_defined_neurons=int(np.count_nonzero((fp < 0) & (fq >= 0))),
first_latency_became_undefined_neurons=int(np.count_nonzero((fp >= 0) & (fq < 0))),
final_delta_v_max_abs=vmax, final_delta_g_max_abs=gmax,
final_ref_changed_neurons=int(np.count_nonzero(pre['ref'][local] != post['ref'][local])),
final_state_units='SI volts; refractory integer ticks',
max_abs_scope='maximum across population neurons at terminal tick; NOT a temporal peak')
if vectors:
result.update(neuron_idx=nodes[local].tolist(), pre_counts=cp.tolist(), post_counts=cq.tolist(),
pre_first_latency_ms_per_neuron=_latencies(fp, dt_ms),
post_first_latency_ms_per_neuron=_latencies(fq, dt_ms))
for key in ('v', 'g', 'ref'):
result['pre_final_' + key] = pre[key][local].tolist()
result['post_final_' + key] = post[key][local].tolist()
return result
def functional_summary(pre, post, *, nodes, populations, steps, dt_ms=.1):
"""Full-state validation plus wholebrain and anatomical-group differences.
The population's earliest event defines the legacy-style first latency.
Per-neuron first events and missingness transitions are additional diagnostics.
A missing latency is None, never zero or evidence of a defined latency shift.
"""
membership = validate_populations(nodes, populations)
a = validate_state(pre, nodes=nodes, steps=steps, dt_ms=dt_ms)
b = validate_state(post, nodes=nodes, steps=steps, dt_ms=dt_ms)
groups = {name: _population_change(pre, post, a, b, nodes,
_positions(nodes, populations[name], name), dt_ms, True) for name in POPULATIONS}
return dict(schema=SCHEMA, full_six_key_state_validated=True, steps=steps, dt_ms=float(dt_ms),
nodes=len(nodes), nodes_raw_sha256=_hash(nodes), membership=membership,
pre_state_raw_sha256=a['hashes'], post_state_raw_sha256=b['hashes'],
wholebrain=_population_change(pre, post, a, b, nodes, np.arange(len(nodes)), dt_ms, False),
populations=groups, MBON_final_delta_g_max_abs=groups['MBON']['final_delta_g_max_abs'],
MBON_final_delta_g_units='SI volts', MBON_final_delta_g_scope='terminal across-neuron maximum, not time peak')
def arm_metrics(pre_by_cue, post_by_cue, *, conditioned_cue, nodes, populations, steps=1200, dt_ms=.1):
"""New fixed, unscaled type-count contrast; neither decoder nor feedback."""
require(conditioned_cue in CUES, 'Unknown conditioned odor label')
require(isinstance(pre_by_cue, Mapping) and isinstance(post_by_cue, Mapping) and
set(pre_by_cue) == set(post_by_cue) == set(CUES), 'Both original odor labels are required')
neutral = next(cue for cue in CUES if cue != conditioned_cue)
summaries = {cue: functional_summary(pre_by_cue[cue], post_by_cue[cue], nodes=nodes,
populations=populations, steps=steps, dt_ms=dt_ms) for cue in CUES}
b, means = {}, {}
for phase in ('pre', 'post'):
b[phase], means[phase] = {}, {}
for cue in CUES:
groups = summaries[cue]['populations']
means[phase][cue] = {name: float(np.mean(groups[name][phase + '_counts'])) for name in ('MBON01', 'MBON12')}
b[phase][cue] = means[phase][cue]['MBON12'] - means[phase][cue]['MBON01']
raw_s = (b['post'][conditioned_cue] - b['post'][neutral]) - (b['pre'][conditioned_cue] - b['pre'][neutral])
separation = {}
for name in SEPARATION_POPULATIONS:
values = {}
for phase in ('pre', 'post'):
first = np.asarray(summaries[CUES[0]]['populations'][name][phase + '_counts'], dtype=np.float64)
second = np.asarray(summaries[CUES[1]]['populations'][name][phase + '_counts'], dtype=np.float64)
values[phase] = float(np.linalg.norm(first - second))
separation[name] = dict(values, gain=values['post'] - values['pre'],
vector='all bound member spike counts', population_semantics=POPULATION_SEMANTICS[name])
return dict(schema=SCHEMA, analysis_status='DRAFT_NEW_HYPOTHESIS', conditioned_cue=conditioned_cue,
neutral_cue=neutral, steps=steps, dt_ms=float(dt_ms), rawB=b, rawS=float(raw_s),
type_mean_counts=means, B_definition='mean full MBON12 counts - mean full MBON01 counts',
B_is_decoder=False, S_orientation='conditioned minus neutral; post minus own pre',
separation=separation, cues=summaries,
historical_implementation_equivalence_claimed=False, learning_pass_claimed=False)
def _finite_tree(value):
if isinstance(value, Mapping):
for item in value.values():
_finite_tree(item)
elif isinstance(value, (list, tuple)):
for item in value:
_finite_tree(item)
elif isinstance(value, (float, np.floating)):
require(math.isfinite(float(value)), 'Nonfinite metric value')
def _bound_metric(metric, cue):
require(isinstance(metric, Mapping) and metric.get('schema') == SCHEMA and
metric.get('analysis_status') == 'DRAFT_NEW_HYPOTHESIS', 'Unexpected metric schema')
_finite_tree(metric)
require(metric['conditioned_cue'] == cue and metric['neutral_cue'] == next(c for c in CUES if c != cue),
'Cue role mismatch')
require(metric['steps'] == 1200 and metric['dt_ms'] == .1, 'Evaluation must use the fixed 120 ms probe')
# Derive gate quantities from recorded raw vectors, not a supplied gate Boolean.
for odor in CUES:
summary = metric['cues'][odor]
require(summary.get('full_six_key_state_validated') is True and summary['steps'] == 1200 and
summary['dt_ms'] == .1 and _sha(summary['nodes_raw_sha256']), 'Unvalidated cue state')
membership = summary['membership']['indices']
require(set(membership) == set(POPULATIONS) and summary['membership']['membership_sha256'] == _json_hash(membership),
'Population membership digest mismatch')
require(summary['membership'].get('population_semantics') == POPULATION_SEMANTICS,
'Population semantics must distinguish old H membership from the new non-MBON readout')
for key in ('pre_state_raw_sha256', 'post_state_raw_sha256'):
require(set(summary[key]) == STATE_KEYS and all(_sha(v) for v in summary[key].values()), 'Missing state digest')
for name in POPULATIONS:
group = summary['populations'][name]
ids, count = group['neuron_idx'], group['neurons']
require(type(count) is int and count > 0 and len(ids) == count and ids == membership[name] and
all(type(v) is int and 0 <= v < 166700 for v in ids) and ids == sorted(set(ids)),
'Empty or mismatched anatomical population')
for phase in ('pre', 'post'):
counts = group[phase + '_counts']
latency = group[phase + '_first_latency_ms_per_neuron']
require(len(counts) == len(latency) == count and
all(type(v) is int and 0 <= v <= 1200 for v in counts), 'Invalid count/latency vector')
for c, value in zip(counts, latency):
require((c == 0 and value is None) or (c > 0 and type(value) in (int, float) and
0 <= value < 120 and value == round(value / .1) * .1), 'Missing/nonphysical first latency')
present = [v for v in latency if v is not None]
require(group[phase + '_first_latency_ms'] == (min(present) if present else None),
'Population first latency differs from raw cells')
for key in ('v', 'g', 'ref'):
values = group[phase + '_final_' + key]
require(len(values) == count and all(type(v) in (int, float) and math.isfinite(v) for v in values),
'Invalid final-state vector')
if key == 'ref':
require(all(type(v) is int and 0 <= v <= 22 for v in values), 'Invalid final ref')
l1 = sum(abs(a - b) for a, b in zip(group['pre_counts'], group['post_counts']))
lp, lq = group['pre_first_latency_ms'], group['post_first_latency_ms']
changed = lp is not None and lq is not None and lp != lq
require(type(group['spike_count_l1']) is int and group['spike_count_l1'] == l1 and
group['defined_first_latency_changed'] is changed and
group['first_latency_delta_ms'] == (None if lp is None or lq is None else lq - lp),
'Functional predicate differs from raw counts/defined latency')
try:
with np.errstate(over='raise', invalid='raise'):
for key in ('v', 'g'):
difference = np.asarray(group['post_final_' + key]) - np.asarray(group['pre_final_' + key])
require(group['final_delta_' + key + '_max_abs'] == float(np.max(np.abs(difference))),
'Final-state maximum differs from raw terminal vectors')
except FloatingPointError as error:
raise ValueError('Nonfinite derived terminal metric') from error
require(not set(membership['MBON01']).intersection(membership['MBON12']), 'Type population overlap')
require(set(membership['MBON']) <= set(membership['legacyDownstream']),
'Legacy downstream must retain the complete MBON population')
require(membership['nonMBONDownstream'] == sorted(set(membership['legacyDownstream']) - set(membership['MBON'])),
'nonMBONDownstream differs from exact legacyDownstream minus MBON')
for name, parent in (('MBON01', 'MBON'), ('MBON12', 'MBON'),
('MBON', 'legacyDownstream'), ('nonMBONDownstream', 'legacyDownstream')):
require(set(membership[name]) <= set(membership[parent]), 'Population is not a subset of its bound parent')
lookup = {v: i for i, v in enumerate(membership[parent])}
for phase in ('pre', 'post'):
for field in ('counts', 'first_latency_ms_per_neuron', 'final_v', 'final_g', 'final_ref'):
key = phase + '_' + field
require(summary['populations'][name][key] == [
summary['populations'][parent][key][lookup[v]] for v in membership[name]],
'Overlapping population vectors disagree with the bound parent')
for phase in ('pre', 'post'):
for odor in CUES:
groups = metric['cues'][odor]['populations']
means = []
for name in ('MBON12', 'MBON01'):
counts = groups[name][phase + '_counts']
require(len(counts) > 0 and all(type(v) is int and 0 <= v <= 1200 for v in counts),
'Empty or invalid type count vector')
means.append(sum(counts) / len(counts))
require(metric['type_mean_counts'][phase][odor][name] == means[-1], 'Type mean differs from raw counts')
require(metric['rawB'][phase][odor] == means[0] - means[1], 'Raw B disagrees with full type vectors')
neutral = metric['neutral_cue']
calculated = (metric['rawB']['post'][cue] - metric['rawB']['post'][neutral]) - (
metric['rawB']['pre'][cue] - metric['rawB']['pre'][neutral])
require(type(metric['rawS']) in (int, float) and metric['rawS'] == calculated, 'Raw S disagrees with signed components')
for name in SEPARATION_POPULATIONS:
calculated = {}
for phase in ('pre', 'post'):
a = metric['cues'][CUES[0]]['populations'][name][phase + '_counts']
b = metric['cues'][CUES[1]]['populations'][name][phase + '_counts']
require(metric['cues'][CUES[0]]['membership'] == metric['cues'][CUES[1]]['membership'],
'A/B population mismatch')
calculated[phase] = float(np.linalg.norm(np.asarray(a, dtype=np.float64) - np.asarray(b, dtype=np.float64)))
require(metric['separation'][name]['pre'] == calculated['pre'] and
metric['separation'][name]['post'] == calculated['post'] and
metric['separation'][name]['gain'] == calculated['post'] - calculated['pre'],
'Separation gain differs from raw count vectors')
require(metric['separation'][name].get('population_semantics') == POPULATION_SEMANTICS[name],
'Separation population semantics mismatch')
return metric
def _execution(arm, name, assay, used_receipts):
require(isinstance(arm, Mapping) and set(arm) == {'metrics', 'execution'}, 'Arm needs metrics and actual execution evidence')
e = arm['execution']
required = {'complete', 'recording_verified', 'receipt_sha256', 'assay', 'epoch_duration_ms',
'cs_duration_ms', 'us_tick', 'completed_epochs', 'reinforcement_opportunities', 'terminal_pairings'}
require(isinstance(e, Mapping) and set(e) == required, 'Incomplete execution evidence')
require(e['complete'] is True and e['recording_verified'] is True and _sha(e['receipt_sha256']),
'All arms require an actual verified completed execution receipt')
require(e['receipt_sha256'] not in used_receipts, 'Execution receipt reused across arms/replicates')
used_receipts.add(e['receipt_sha256'])
require(e['assay'] == assay and e['epoch_duration_ms'] == (120. if assay == 'normal' else 180.) and
e['cs_duration_ms'] == 100. and type(e['us_tick']) is int and
e['us_tick'] == (900 if assay == 'normal' else 1500), 'Normal/delayed control timing mismatch')
require(type(e['completed_epochs']) is int and e['completed_epochs'] == (150 if name == 'unpaired' else 100),
'Incomplete arm epoch inventory')
require(type(e['reinforcement_opportunities']) is int and e['reinforcement_opportunities'] == (0 if name == 'CS-only' else 50),
'Wrong arm US opportunity inventory')
require(type(e['terminal_pairings']) is int and e['terminal_pairings'] == 50, 'Only fixed terminal count 50 may gate')
def evaluate_acquisition(replicates, *, assay, mode, dan_type, conditioned_cue, registered_training_streams):
"""Strict new normal/delayed 3-of-5 SAME-training-replicate conjunction.
Each row has replicate_id, replicate_unit='independent_training',
training_stream_ids (all independent source IDs, not aliases),
evaluation_stream_ids, assay/mode/dan_type/conditioned_cue, and seven arms.
Arm = {metrics: arm_metrics(...), execution: complete receipt declaration}.
registered_training_streams maps exactly five predeclared replicate IDs to
their source-ID lists. Aliased controls stay within a row; evaluation cannot
become new training replication. This pure function does NOT verify a file
receipt/seed derivation; the coordinator must independently do so.
"""
require(assay in ('normal', 'delayed') and mode in MODES and dan_type in DAN_TYPES and conditioned_cue in CUES,
'Unknown raw assay/mode/DAN/odor label')
require(isinstance(registered_training_streams, Mapping) and len(registered_training_streams) == 5,
'Require five predeclared independent training replicates')
registered, training_all = {}, set()
for identity, sources in registered_training_streams.items():
_token(identity, 'training replicate identity')
require(isinstance(sources, (tuple, list)) and len(sources) > 0, 'Missing actual training source identities')
values = tuple(_token(v, 'training source identity') for v in sources)
require(len(set(values)) == len(values) and not training_all.intersection(values),
'Training source reused across independent replicates')
training_all.update(values)
registered[identity] = values
require(isinstance(replicates, (tuple, list)) and len(replicates) == 5, 'Exactly five training rows required; do not count evaluation draws')
used_ids, used_eval, receipts, reports = set(), set(), set(), []
for row in replicates:
keys = {'replicate_id', 'replicate_unit', 'training_stream_ids', 'evaluation_stream_ids',
'assay', 'mode', 'dan_type', 'conditioned_cue', 'arms'}
require(isinstance(row, Mapping) and set(row) == keys, 'Invalid replicate schema')
identity = row['replicate_id']
require(identity in registered and identity not in used_ids and row['replicate_unit'] == 'independent_training',
'Unknown/duplicate training identity or evaluation pseudo-replication')
used_ids.add(identity)
require(isinstance(row['training_stream_ids'], (list, tuple)) and
tuple(row['training_stream_ids']) == registered[identity], 'Training lineage does not match preregistration')
require((row['assay'], row['mode'], row['dan_type'], row['conditioned_cue']) == (assay, mode, dan_type, conditioned_cue),
'Cannot pool modes, DAN branches, cue assignments or timing suites')
ev = row['evaluation_stream_ids']
require(isinstance(ev, (tuple, list)) and len(ev) > 0, 'Missing independent evaluation identity')
ev = tuple(_token(v, 'evaluation identity') for v in ev)
require(len(set(ev)) == len(ev) and not training_all.intersection(ev) and not used_eval.intersection(ev),
'Evaluation sources overlap training or another training replicate')
used_eval.update(ev)
require(isinstance(row['arms'], Mapping) and set(row['arms']) == set(ARMS), 'All six actual controls and paired arm are mandatory')
metrics = {}
for name in ARMS:
_execution(row['arms'][name], name, assay, receipts)
metrics[name] = _bound_metric(row['arms'][name]['metrics'], conditioned_cue)
baseline = metrics['paired']
# State baseline and populations must really be matched for each control.
for metric in metrics.values():
for cue in CUES:
a, b = baseline['cues'][cue], metric['cues'][cue]
require(a['membership'] == b['membership'] and a['nodes_raw_sha256'] == b['nodes_raw_sha256'] and
a['pre_state_raw_sha256'] == b['pre_state_raw_sha256'], 'Control population or pre-baseline mismatch')
s = baseline['rawS']
control_s = {name: metrics[name]['rawS'] for name in CONTROLS}
direction = s > 0 if dan_type == 'PAM15' else s < 0
beyond = {name: (s > value if dan_type == 'PAM15' else s < value) for name, value in control_s.items()}
neutral = baseline['neutral_cue']
c = baseline['cues'][conditioned_cue]['populations']['MBON']
n = baseline['cues'][neutral]['populations']['MBON']
functional = c['spike_count_l1'] > 0 or c['defined_first_latency_changed'] is True
predicates = dict(raw_signed_direction=bool(direction), beyond_each_actual_control=all(beyond.values()),
conditioned_final_g_exceeds_neutral=c['final_delta_g_max_abs'] > n['final_delta_g_max_abs'],
conditioned_count_or_defined_population_first_latency_change=bool(functional),
all_MBON_AB_euclidean_gain_positive=baseline['separation']['MBON']['gain'] > 0,
legacyDownstream_AB_euclidean_gain_positive=baseline['separation']['legacyDownstream']['gain'] > 0,
nonMBONDownstream_AB_euclidean_gain_positive=baseline['separation']['nonMBONDownstream']['gain'] > 0)
reports.append(dict(replicate_id=identity, rawS=s, raw_control_S=control_s,
population_separation_gains={name: baseline['separation'][name]['gain'] for name in SEPARATION_POPULATIONS},
beyond_controls= beyond, predicates=predicates, conjunction_pass=all(predicates.values()),
legacy_audit=dict(L_like_functional_with_explicit_missing_latency=bool(
predicates['conditioned_final_g_exceeds_neutral'] and functional),
H_like_MBON_positive_gain=predicates['all_MBON_AB_euclidean_gain_positive'],
H_like_downstream_population='legacyDownstream',
H_like_downstream_positive_gain=predicates['legacyDownstream_AB_euclidean_gain_positive'],
original_implementation_equivalence_claimed=False),
new_hypothesis_audit=dict(nonMBONDownstream_positive_gain=
predicates['nonMBONDownstream_AB_euclidean_gain_positive'],
inherited_from_old_H=False, either_downstream_may_rescue_other=False)))
reports.sort(key=lambda r: r['replicate_id'])
successful = sum(r['conjunction_pass'] for r in reports)
return dict(schema=SCHEMA, status='DRAFT-PROPOSAL-GATE-PASS' if successful >= 3 else 'DRAFT-PROPOSAL-GATE-FAIL',
proposal_gate_pass=successful >= 3, scientific_learning_pass_claimed=False, candidate_generated=False,
assay=assay, mode=mode, dan_type=dan_type, conditioned_cue=conditioned_cue,
required_training_replicates=5, successful_same_replicate_conjunctions=successful, required_majority=3,
actual_control_receipts=len(receipts) - 5, evaluation_draws_count_as_replicates=False,
execution_receipt_authority='caller must verify finalized receipts and seed registry; pure metrics do not read files',
historical_full_H_mean_control_rule_computed=False, replicates=reports)
def legacy_h_mean_control_audit(protocol_mbon_gains):
"""Old H scalar predicate only, kept separate from the new signed gate.
Input uses the ORIGINAL five protocol labels, each with five already-bound
training-replicate MBON Euclidean gains. The caller must explicitly define
any AN-to-old-protocol mapping; this function never picks a DAN/control or
pools AN cells. It is not the entire old H gate or a reproduced old result.
"""
names = ('appetitive-conditioning', 'aversive-conditioning', 'd-zero-control',
'random-reward-control', 'frozen-brain-control')
require(isinstance(protocol_mbon_gains, Mapping) and set(protocol_mbon_gains) == set(names),
'Legacy audit requires exactly the five original H protocol labels')
means = {}
for name in names:
values = protocol_mbon_gains[name]
require(isinstance(values, (list, tuple)) and len(values) == 5 and
all(type(v) in (int, float) and math.isfinite(v) for v in values),
'Legacy audit needs five finite, separately bound training-replicate gains')
means[name] = float(np.mean(values))
require(math.isfinite(means[name]), 'Nonfinite legacy mean')
floor, ceiling = min(means[names[0]], means[names[1]]), max(means[name] for name in names[2:])
return dict(schema=SCHEMA, audit='legacy_H_mean_control_predicate_only', protocol_means=means,
condition_floor=floor, control_ceiling=ceiling, predicate_pass=floor > ceiling,
source_reference='frozen phase4h.py:412-414', full_old_H_gate_computed=False,
scientific_learning_pass_claimed=False, AN_to_legacy_protocol_mapping_authority='caller; no automatic pooling')
def unavailable_assay(assay):
"""Do not turn a terminal delta or fresh reset into a retention/reversal PASS."""
require(assay in ('reversal', 'retention'), 'Unknown unavailable assay')
missing = {'reversal': ['bound acquisition/reversal/no-reversal lineage',
'original pre baseline and original cue orientation', 'terminal reversal count and both cue event outputs'],
'retention': ['validated continuous-state checkpoint/queue/RNG schema',
'bound same idle trajectory at 1 s and 10 s', 'matched frozen continuation and non-feedback probes']}
return dict(schema=SCHEMA, assay=assay, status='NOT-IMPLEMENTED', proposal_gate_pass=None,
scientific_learning_pass_claimed=False, candidate_generated=False, missing=missing[assay])
@@ -0,0 +1,118 @@
"""Explicit, order-independent random streams for an unregistered protocol.
This module does not select odors, generate neural input, or run experiments.
Every independent stream has a semantic key and a SHA-derived 63-bit seed.
Intentional common-random-number controls must be declared as aliases of a
same-split stream. Aliases are never counted as independent replicates.
"""
from __future__ import annotations
import hashlib
import json
SCHEMA = "phase4an-seed-registry-v1"
SPLITS = frozenset(("training", "evaluation", "design"))
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 _key(key):
_require(isinstance(key, (tuple, list)) and len(key) > 0,
"A nonempty list/tuple semantic key is required")
_require(all(type(v) is str and v and v.strip() == v for v in key),
"Semantic key tokens must be nonempty, unpadded strings")
return tuple(key)
def seed_for(master, split, key):
_require(type(master) is int and 0 <= master < 2**63, "master must be a 63-bit integer")
_require(type(split) is str and split in SPLITS, "Unknown stream split")
key = _key(key)
message = _canonical([SCHEMA, master, split, list(key)]).encode("utf-8")
full_sha = hashlib.sha256(message).hexdigest()
return int(full_sha[:16], 16) & ((1 << 63) - 1), full_sha
class SeedRegistry:
"""Fail closed on duplicate keys, collisions or cross-split aliasing.
No collision rehashing or seed search is performed: such a failure must be
resolved before registration, never after observing a neural response.
"""
def __init__(self, master):
seed_for(master, "design", ("validate-master",))
self.master = master
self._streams = {}
self._aliases = {}
self._seed_to_key = {}
def add(self, split, key):
key = _key(key)
identity = (split, key)
_require(identity not in self._streams and identity not in self._aliases,
"Duplicate semantic stream key")
seed, full_sha = seed_for(self.master, split, key)
_require(seed not in self._seed_to_key, "Seed collision: stop before registration")
row = dict(split=split, key=list(key), seed=seed, derivation_sha256=full_sha)
self._streams[identity] = row
self._seed_to_key[seed] = identity
return seed
def alias(self, split, key, source_split, source_key, *, reason):
"""Declare paired controls, never silently create/reuse a stream."""
key, source_key = _key(key), _key(source_key)
identity, source = (split, key), (source_split, source_key)
_require(split == source_split and split in SPLITS,
"Cross-split random-stream sharing is forbidden")
_require(identity not in self._streams and identity not in self._aliases,
"Duplicate semantic stream key")
_require(source in self._streams, "Alias must target an independent stream, not another alias")
_require(type(reason) is str and bool(reason.strip()), "Intentional sharing requires a reason")
seed = self._streams[source]["seed"]
self._aliases[identity] = dict(split=split, key=list(key), seed=seed,
source_split=source_split, source_key=list(source_key), reason=reason,
independent_replicate=False)
return seed
def manifest(self):
streams = [dict(self._streams[k]) for k in sorted(self._streams)]
aliases = [dict(self._aliases[k]) for k in sorted(self._aliases)]
split_counts = {split: sum(r["split"] == split for r in streams) for split in sorted(SPLITS)}
result = dict(schema=SCHEMA, master=self.master,
algorithm="SHA-256 canonical JSON [schema,master,split,key]; first64 bits masked to63",
independent_streams=streams, aliases=aliases,
independent_count=len(streams), alias_count=len(aliases), split_counts=split_counts,
all_independent_seeds_unique=True, training_evaluation_seed_overlap=0,
aliases_count_as_independent_replicates=False,
training_executed=False, protocol_approval_claimed=False)
result["payload_sha256"] = hashlib.sha256(_canonical(result).encode("utf-8")).hexdigest()
# JSON round-trip makes nested data independent of the mutable registry.
return json.loads(_canonical(result))
def validate_manifest(manifest):
"""Reconstruct every seed/alias and require the complete canonical payload."""
_require(isinstance(manifest, dict) and manifest.get("schema") == SCHEMA, "Invalid seed manifest")
payload = {key: value for key, value in manifest.items() if key != "payload_sha256"}
actual_sha = hashlib.sha256(_canonical(payload).encode("utf-8")).hexdigest()
_require(manifest.get("payload_sha256") == actual_sha, "Actual manifest payload SHA mismatch")
rebuilt = SeedRegistry(manifest["master"])
for row in manifest["independent_streams"]:
rebuilt.add(row["split"], row["key"])
for row in manifest["aliases"]:
rebuilt.alias(row["split"], row["key"], row["source_split"], row["source_key"], reason=row["reason"])
# Python dict equality conflates False/0 and 1/1.0; the serialized contract
# must retain JSON Boolean versus integer versus floating-point identity.
_require(_canonical(manifest) == _canonical(rebuilt.manifest()),
"Seed manifest differs from full independent reconstruction")
return True
@@ -0,0 +1,153 @@
import unittest
import numpy as np
import pandas as pd
import biological_contract as b
def fixture():
types = ["KC-a", "KC-b", "PAM15", "PPL103", "MBON01", "MBON12", "other-DAN"]
classes = ["Kenyon_Cell", "Kenyon_Cell", "DAN", "DAN", "MBON", "MBON", "DAN"]
instances = ["KC-a_L", "KC-b_R", "PAM15(y5B'2a)_L", "PPL103(y2a'1)_R",
"MBON01(y5B'2a)_L", "MBON12(y2a'1)_R", "other-DAN"]
meta = pd.DataFrame(dict(neuron_idx=np.arange(7, dtype=np.int32),
body_id=np.arange(7, dtype=np.uint64) + np.uint64(2**63),
annotation_type=pd.Series(types, dtype=object), annotation_class=pd.Series(classes, dtype=object),
annotation_instance=pd.Series(instances, dtype=object)))
dan = meta.iloc[[2, 3, 6]].drop(columns="annotation_class").copy()
mbon = meta.iloc[[4, 5]].drop(columns="annotation_class").copy()
dan["mb_compartment_verbatim"] = ["y5B'2a", "y2a'1", None]
dan["mb_compartment_mapping_status"] = ["explicit_single_parenthesized_token"] * 2 + ["no_parenthesized_token"]
mbon["mb_compartment_verbatim"] = ["y5B'2a", "y2a'1"]
mbon["mb_compartment_mapping_status"] = "explicit_single_parenthesized_token"
src = np.array([0, 0, 1, 1, 2, 3], np.int32)
dst = np.array([4, 5, 4, 5, 4, 5], np.int32)
raw = np.array([4, 8, 12, 16, 20, 24], np.int64)
def table(ids):
rows = []
for i in ids:
row = dict(edge_id=i, raw_synapse_count=int(raw[i]))
for label, node in (("source", src[i]), ("target", dst[i])):
for key in ("neuron_idx", "body_id", "annotation_type", "annotation_instance"):
row[label + "_" + key] = meta.iloc[node][key]
row["target_compartment_verbatim"] = "y5B'2a" if dst[i] == 4 else "y2a'1"
rows.append(row)
out = pd.DataFrame(rows)
for key in ("source_neuron_idx", "target_neuron_idx"):
out[key] = out[key].astype(np.int32)
for key in ("source_body_id", "target_body_id"):
out[key] = out[key].astype(np.uint64)
# Match the observed frozen parquet object dtype; local pandas 3 may
# infer its new string extension dtype from constructor literals.
for prefix in ("source", "target"):
for suffix in ("annotation_type", "annotation_instance"):
out[prefix + "_" + suffix] = out[prefix + "_" + suffix].astype(object)
out["edge_id"] = out.edge_id.astype(np.int64)
out["raw_synapse_count"] = out.raw_synapse_count.astype(np.int64)
return out
candidates = table(range(4))
candidates["plasticity_polarity"] = np.int64(-1)
confirmed = table([4, 5])
confirmed["source_compartment_verbatim"] = confirmed.target_compartment_verbatim
confirmed["exact_compartment_match"] = True
return meta, dan, mbon, confirmed, candidates, src, dst, raw
class Biology(unittest.TestCase):
def test_complete_graph_binds_every_edge_array_length(self):
meta, *_, src, dst, raw = fixture()
graph = dict(nodes=np.arange(7, dtype=np.int32), edge_id=np.arange(6, dtype=np.int64),
src=src, dst=dst, signed_raw=raw)
b._complete_graph_identity(meta, graph, n=7, e=6)
for names in (("src",), ("dst",), ("signed_raw",), ("src", "dst", "signed_raw")):
changed = dict(graph)
for name in names:
changed[name] = changed[name][:-1]
with self.subTest(names=names), self.assertRaisesRegex(ValueError, "endpoint/weight lengths"):
b._complete_graph_identity(meta, changed, n=7, e=6)
for name in ("nodes", "edge_id"):
changed = dict(graph)
changed[name] = changed[name].reshape(1, -1)
with self.subTest(name=name), self.assertRaises(ValueError):
b._complete_graph_identity(meta, changed, n=7, e=6)
def test_exact_id_compartment_and_real_edge_contract(self):
values = fixture()
populations, audit = b.audit_interface_tables(*values)
self.assertEqual(populations["PAM15"].tolist(), [2])
self.assertEqual(populations["MBON12"].tolist(), [5])
self.assertEqual(audit["whole_DAN_mapping_unknown"], 1)
self.assertFalse(audit["global_unknowns_filled"])
self.assertEqual(audit["all_candidate_edges_retained"], 4)
self.assertEqual(audit["interfaces"]["PAM15"]["candidate_contacts"], 16)
self.assertEqual(audit["interfaces"]["PPL103"]["confirmed_DAN_MBON_contacts"], 24)
self.assertGreater(audit["interfaces"]["PAM15"]["DAN_members"][0]["body_id"], 2**63)
self.assertTrue(pd.isna(values[1].iloc[-1].mb_compartment_verbatim))
def test_unknown_selected_mapping_is_not_filled_from_type(self):
for field, new_value in (("mb_compartment_verbatim", None),
("mb_compartment_mapping_status", "unknown")):
values = fixture()
values[1].loc[2, field] = new_value
with self.assertRaisesRegex(ValueError, "Unknown or mismatched"):
b.audit_interface_tables(*values)
def test_changed_body_id_or_type_or_instance_rejected(self):
for field, new_value in (("body_id", np.uint64(5)), ("annotation_type", "PAM_OTHER"),
("annotation_instance", "inferred")):
values = fixture()
values[1].loc[2, field] = new_value
with self.assertRaisesRegex(ValueError, "Identity field mismatch"):
b.audit_interface_tables(*values)
def test_unmapped_dan_cannot_be_silently_removed(self):
values = list(fixture())
values[1] = values[1].iloc[:2].copy()
with self.assertRaisesRegex(ValueError, "complete exact-annotation"):
b.audit_interface_tables(*values)
def test_missing_actual_edge_fails_not_guessed(self):
values = list(fixture())
values[3] = values[3].iloc[:1].copy()
with self.assertRaisesRegex(ValueError, "unknown or mismatched actual target"):
b.audit_interface_tables(*values)
def test_edge_identity_and_contacts_and_polarity(self):
changes = ((3, "source_body_id", np.uint64(8)),
(3, "target_annotation_type", "MBON_OTHER"),
(4, "raw_synapse_count", np.int64(1)),
(4, "plasticity_polarity", np.int64(1)))
for which, field, value in changes:
values = fixture()
values[which].loc[values[which].index[0], field] = value
with self.subTest(field=field), self.assertRaises(ValueError):
b.audit_interface_tables(*values)
def test_false_or_nonboolean_confirmed_flag_rejected(self):
for value in (False, "True"):
values = fixture()
values[3]["exact_compartment_match"] = value
with self.assertRaisesRegex(ValueError, "unknown compartment"):
b.audit_interface_tables(*values)
def test_graph_endpoint_and_sign_change_rejected(self):
for where in (5, 6, 7):
values = fixture()
values[where][0] = -1 if where == 7 else 6
with self.assertRaises(ValueError):
b.audit_interface_tables(*values)
def test_schema_and_duplicate_identity_rejected(self):
values = list(fixture())
values[0] = values[0].drop(columns="annotation_class")
with self.assertRaises(ValueError):
b.audit_interface_tables(*values)
values = fixture()
values[0].loc[1, "body_id"] = values[0].loc[0, "body_id"]
with self.assertRaises(ValueError):
b.audit_interface_tables(*values)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,258 @@
"""Synthetic CPU/mocked-CUDA contract tests; never run a real input generator."""
import ast
import hashlib
import json
from pathlib import Path
import types
import unittest
from unittest.mock import patch
import numpy as np
import conditioning_inputs as c
def req(rows):
values = np.asarray(rows, np.int32).reshape(-1, 2)
return {'tick': values[:, 0].copy(), 'neuron_idx': values[:, 1].copy()}
class FakeTensor:
def __init__(self, values, device):
self.values = np.array(values, copy=True)
self.device, self.dtype = device, self.values.dtype
def __getitem__(self, key): return FakeTensor(self.values[key], self.device)
def __lt__(self, other): return FakeTensor(self.values < other.values, self.device)
def cpu(self): return self
def numpy(self): return self.values.copy()
class FakeTorch:
"""Observable adapter, not a CUDA or random-number emulation."""
float32 = np.dtype(np.float32)
__version__ = 'SYNTHETIC-CONTRACT-MOCK-NOT-TORCH'
version = types.SimpleNamespace(cuda=None)
def __init__(self, uniforms, truncate=False):
self.uniforms = uniforms
self.calls, self.seeds, self.truncate = [], [], truncate
self.cuda = types.SimpleNamespace(is_available=lambda: True,
get_device_name=lambda device: 'SYNTHETIC-NOT-A-REAL-GPU', get_device_capability=lambda device: (0, 0),
get_device_properties=lambda device: types.SimpleNamespace())
def get_default_dtype(self): return self.float32
def device(self, name): return name
def Generator(self, device):
owner = self
class Generator:
def manual_seed(self, value):
owner.seeds.append(value); self.seed = value & 0xffffffff if owner.truncate else value
def initial_seed(self): return self.seed
self.calls.append(('generator', device)); return Generator()
def as_tensor(self, values, dtype, device):
self.calls.append(('probability', np.array(values, copy=True), dtype, device))
return FakeTensor(values, device)
def rand(self, shape, generator, device):
self.calls.append(('rand', shape, generator.initial_seed(), device))
if shape != self.uniforms.shape: raise AssertionError('Unexpected draw shape')
return FakeTensor(self.uniforms, device)
class InputsTests(unittest.TestCase):
def setUp(self):
self.targets = np.array([19, 3], np.int32)
self.rates = np.array([5000, 10000], np.float32)
def test_frozen_probability_dtype_order_signed_zero_and_no_mutation(self):
targets = np.arange(6, dtype=np.int32)
rates = np.array([0., -0., np.nextafter(np.float32(0), np.float32(1)), .1, 123.456, 10000.], np.float32)
before = rates.tobytes(); rates.setflags(write=False)
actual = c.probabilities_f32(targets, rates)
self.assertEqual(actual.dtype, np.float32)
self.assertEqual(actual.tobytes(), (rates * .1 / 1000.).tobytes())
self.assertEqual(rates.tobytes(), before)
def test_uniform_threshold_is_strict_and_bernoulli_not_poisson_counts(self):
p = np.array([0, .5, 1], np.float32)
u = np.array([[0, .5, np.nextafter(np.float32(1), np.float32(0))], [0, .499, 0]], np.float32)
forced = c.forced_from_uniforms(u, p)
np.testing.assert_array_equal(forced, [[False, False, True], [False, True, True]])
self.assertEqual(forced.dtype, np.bool_)
def test_original_PN_column_order_then_sparse_tick_global_sort_only(self):
forced = np.array([[True, True], [True, False], [False, True]], np.bool_)
before = forced.copy(); self.targets.setflags(write=False)
result = c.requests_from_forced(forced, self.targets, cs_start_tick=0, cs_stop_tick=3)
np.testing.assert_array_equal(result['requests']['tick'], [0, 0, 1, 2])
np.testing.assert_array_equal(result['requests']['neuron_idx'], [3, 19, 19, 3])
np.testing.assert_array_equal(forced, before)
self.assertEqual(result['audit']['duplicate_requests_removed'], 0)
def test_half_open_window_masks_full_matrix_including_empty_window(self):
forced = np.ones((5, 2), np.bool_)
result = c.requests_from_forced(forced, self.targets, cs_start_tick=1, cs_stop_tick=4)
self.assertEqual(result['audit']['full_matrix_boolean_hits'], 10)
self.assertEqual(result['audit']['outside_CS_boolean_hits_masked'], 4)
self.assertEqual(set(result['requests']['tick']), {1, 2, 3})
empty = c.requests_from_forced(forced, self.targets, cs_start_tick=2, cs_stop_tick=2)
self.assertEqual(len(empty['requests']['tick']), 0)
self.assertEqual(empty['audit']['outside_CS_boolean_hits_masked'], 10)
def test_full_63bit_seed_single_full_draw_and_mock_scope_receipt(self):
seed = (1 << 62) + (1 << 32) + 19
fake = FakeTorch(np.array([[.2, .6], [.7, .7], [.1, .2], [.8, .1], [.1, .1]], np.float32))
with patch.object(c, '_load_torch', return_value=fake):
result = c.generate_cs_requests(self.targets, self.rates, seed=seed, steps=5, cs_start_tick=1, cs_stop_tick=4,
device='cuda:0', max_uniform_draws=10)
self.assertEqual(fake.seeds, [seed]); draws = [x for x in fake.calls if x[0] == 'rand']
self.assertEqual(draws, [('rand', (5, 2), seed, 'cuda:0')])
a = result['audit']; self.assertEqual(a['seed_hex'], f'{seed:016x}')
self.assertEqual(a['numpy_version'], np.__version__)
self.assertEqual(a['torch_version'], fake.__version__)
self.assertEqual(a['device_name'], 'SYNTHETIC-NOT-A-REAL-GPU')
self.assertFalse(a['CUDA_sequence_equivalence_tested_here'])
self.assertFalse(a['CUDA_high_seed_bit_sequence_influence_tested_here'])
self.assertFalse(a['full_old_L_input_identity_claimed'])
self.assertEqual(a['uniform_scalar_draws'], 10)
self.assertEqual((a['max_uniform_draws'], a['uniform_matrix_bytes']), (10, 40))
self.assertEqual(a['device_identity'], {'uuid': None, 'pci_bus_id': None, 'query_error': None})
np.testing.assert_array_equal(result['requests']['tick'], [1, 2, 2, 3])
np.testing.assert_array_equal(result['requests']['neuron_idx'], [3, 3, 19, 3])
json.dumps(a, allow_nan=False)
def test_high_seed_bits_preserved_and_bad_consumer_detected_before_draw(self):
for seed in (0, 17, (1 << 32) + 17, (1 << 63) - 1):
fake = FakeTorch(np.zeros((1, 2), np.float32))
with patch.object(c, '_load_torch', return_value=fake):
result = c.generate_cs_requests(self.targets, self.rates, seed=seed, steps=1, cs_start_tick=0, cs_stop_tick=1,
device='cuda:0', max_uniform_draws=2)
self.assertEqual(result['audit']['generator_initial_seed'], seed)
self.assertEqual(fake.seeds, [seed])
fake = FakeTorch(np.zeros((1, 2), np.float32), truncate=True)
with patch.object(c, '_load_torch', return_value=fake), self.assertRaisesRegex(ValueError, 'complete semantic seed'):
c.generate_cs_requests(self.targets, self.rates, seed=(1 << 32) + 17, steps=1, cs_start_tick=0, cs_stop_tick=1,
device='cuda:0', max_uniform_draws=2)
self.assertFalse(any(x[0] == 'rand' for x in fake.calls))
def test_invalid_seed_clock_or_rate_never_imports_torch(self):
for seed in (-1, 2**63, True, np.int64(3), 3.):
with patch.object(c, '_load_torch') as loader, self.assertRaises(ValueError):
c.generate_cs_requests(self.targets, self.rates, seed=seed, steps=5, cs_start_tick=0, cs_stop_tick=5,
device='cuda:0', max_uniform_draws=10)
loader.assert_not_called()
for start, stop in ((-1, 1), (2, 1), (0, 6), (False, 1)):
with patch.object(c, '_load_torch') as loader, self.assertRaises(ValueError):
c.generate_cs_requests(self.targets, self.rates, seed=3, steps=5, cs_start_tick=start, cs_stop_tick=stop,
device='cuda:0', max_uniform_draws=10)
loader.assert_not_called()
def test_invalid_arrays_and_no_clipping_or_duplicate_merging(self):
for rates in (np.array([-1, 2], np.float32), np.array([np.nan, 2], np.float32), np.array([np.inf, 2], np.float32),
np.array([10001, 2], np.float32), self.rates.astype(np.float64), self.rates[:1]):
with self.assertRaises(ValueError): c.probabilities_f32(self.targets, rates)
for targets in (np.array([3, 3], np.int32), np.array([-1, 3], np.int32), np.array([166700, 3], np.int32), self.targets.astype(np.int64)):
with self.assertRaises(ValueError): c.probabilities_f32(targets, self.rates)
with self.assertRaises(ValueError): c.probabilities_f32(self.targets, self.rates, dt_ms=.2)
with self.assertRaises(ValueError): c.requests_from_forced(np.ones((2, 2), np.uint8), self.targets, cs_start_tick=0, cs_stop_tick=2)
def test_no_cpu_fallback_or_changed_default_float_dtype(self):
for variant in ('cpu', 'unavailable', 'float64'):
fake = FakeTorch(np.zeros((1, 2), np.float32)); device = 'cuda:0'
if variant == 'cpu': device = 'cpu'
if variant == 'unavailable': fake.cuda.is_available = lambda: False
if variant == 'float64': fake.get_default_dtype = lambda: np.dtype(np.float64)
with patch.object(c, '_load_torch', return_value=fake), self.assertRaises(ValueError):
c.generate_cs_requests(self.targets, self.rates, seed=3, steps=1, cs_start_tick=0, cs_stop_tick=1,
device=device, max_uniform_draws=2)
self.assertFalse(any(x[0] == 'rand' for x in fake.calls))
def test_explicit_device_and_budget_required_and_budget_checked_before_torch(self):
common = dict(seed=3, steps=5, cs_start_tick=0, cs_stop_tick=1)
for missing in ({}, {'device': 'cuda:0'}, {'max_uniform_draws': 10}):
with patch.object(c, '_load_torch') as loader, self.assertRaises(TypeError):
c.generate_cs_requests(self.targets, self.rates, **common, **missing)
loader.assert_not_called()
for budget in (0, -1, 9, True, 10., np.int64(10)):
with patch.object(c, '_load_torch') as loader, self.assertRaises(ValueError):
c.generate_cs_requests(self.targets, self.rates, **common, device='cuda:0', max_uniform_draws=budget)
loader.assert_not_called()
# Full 5x2 matrix admission, not merely the 1-tick active CS window.
fake = FakeTorch(np.zeros((5, 2), np.float32))
with patch.object(c, '_load_torch', return_value=fake):
result = c.generate_cs_requests(self.targets, self.rates, **common, device='cuda:1', max_uniform_draws=10)
self.assertEqual(result['audit']['execution_device'], 'cuda:1')
self.assertEqual(result['audit']['uniform_scalar_draws'], 10)
def test_optional_device_uuid_recorded_as_reported_or_explicitly_unavailable(self):
fake = FakeTorch(np.zeros((1, 2), np.float32))
fake.cuda.get_device_properties = lambda device: types.SimpleNamespace(uuid='SYNTHETIC-UUID', pci_bus_id='SYNTHETIC-PCI')
args = dict(seed=3, steps=1, cs_start_tick=0, cs_stop_tick=1, device='cuda:7', max_uniform_draws=2)
with patch.object(c, '_load_torch', return_value=fake):
audit = c.generate_cs_requests(self.targets, self.rates, **args)['audit']
self.assertEqual(audit['device_identity'], {'uuid': 'SYNTHETIC-UUID', 'pci_bus_id': 'SYNTHETIC-PCI', 'query_error': None})
self.assertFalse(audit['device_identity_verified_against_external_pin'])
def unavailable(device): raise RuntimeError('Synthetic property unavailable')
fake.cuda.get_device_properties = unavailable
with patch.object(c, '_load_torch', return_value=fake):
audit = c.generate_cs_requests(self.targets, self.rates, **args)['audit']
self.assertIsNone(audit['device_identity']['uuid'])
self.assertEqual(audit['device_identity']['query_error'], 'RuntimeError: Synthetic property unavailable')
def test_returned_audited_arrays_are_owned_readonly_and_inputs_not_frozen(self):
forced = np.ones((2, 2), np.bool_)
generated = c.requests_from_forced(forced, self.targets, cs_start_tick=0, cs_stop_tick=2)
merged = c.merge_requests(req([[0, 9]]), req([[1, 8]]), steps=2)
for value in (*generated['requests'].values(), *merged['requests'].values(), merged['source_mask']):
self.assertTrue(value.flags.owndata)
self.assertFalse(value.flags.writeable)
with self.assertRaises(ValueError): value[0] = 0
self.assertTrue(forced.flags.writeable)
self.assertTrue(self.targets.flags.writeable)
def test_empty_targets_and_p0_p1_are_explicit_valid_limits(self):
result = c.requests_from_forced(np.zeros((3, 0), np.bool_), np.empty(0, np.int32), cs_start_tick=0, cs_stop_tick=3)
self.assertEqual(result['requests']['tick'].dtype, np.int32); self.assertEqual(len(result['requests']['tick']), 0)
np.testing.assert_array_equal(c.probabilities_f32(self.targets, np.array([0, 10000], np.float32)), [0, 1])
def test_merging_disjoint_sources_is_sorted_without_mutating_inputs(self):
cs, us = req([[0, 9], [3, 4]]), req([[0, 2], [2, 5]])
for value in [*cs.values(), *us.values()]: value.setflags(write=False)
result = c.merge_requests(cs, us, steps=4)
np.testing.assert_array_equal(result['requests']['tick'], [0, 0, 2, 3])
np.testing.assert_array_equal(result['requests']['neuron_idx'], [2, 9, 5, 4])
np.testing.assert_array_equal(result['source_mask'], [2, 1, 2, 1])
self.assertEqual(result['audit']['explicit_boolean_overlap_collapses'], 0)
def test_collision_rejected_by_default_or_explicitly_audited_boolean_union(self):
cs, us = req([[0, 9], [3, 4]]), req([[0, 9], [2, 5], [3, 4]])
with self.assertRaises(c.RequestConflictError) as error: c.merge_requests(cs, us, steps=4)
self.assertEqual(error.exception.audit['cross_source_conflict_count'], 2)
result = c.merge_requests(cs, us, steps=4, conflict_policy='boolean_union')
np.testing.assert_array_equal(result['source_mask'], [3, 2, 3])
a = result['audit']; self.assertEqual((a['CS_attempts'], a['US_attempts'], a['output_requests']), (2, 3, 3))
self.assertEqual(a['explicit_boolean_overlap_collapses'], 2)
self.assertEqual([x['combined_boolean_requests'] for x in a['conflicts']], [1, 1])
json.dumps(a, allow_nan=False)
def test_within_source_duplicate_unsorted_and_bad_range_never_deduped(self):
for rows in ([[0, 3], [0, 3]], [[1, 3], [0, 3]], [[4, 3]], [[0, -1]]):
with self.assertRaises(ValueError): c.merge_requests(req(rows), req([]), steps=4, conflict_policy='boolean_union')
with self.assertRaises(ValueError): c.merge_requests(req([]), req([]), steps=4, conflict_policy='clip')
def test_frozen_AK_pure_conversion_AST_matches_for_full_window(self):
local = Path(__file__).resolve().parent.parent / 'apl-timing-validation-v1/encoder_inputs.py'
raw = local.read_bytes(); self.assertEqual(hashlib.sha256(raw).hexdigest(), c.AK_ENCODER_SHA)
tree = ast.parse(raw); node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'requests_from_forced')
namespace = {'np': np}; exec(compile(ast.Module(body=[node], type_ignores=[]), str(local), 'exec'), namespace)
forced = np.array([[True, True], [False, True], [True, False]], np.bool_)
expected = namespace['requests_from_forced'](forced, self.targets)
actual = c.requests_from_forced(forced, self.targets, cs_start_tick=0, cs_stop_tick=3)['requests']
np.testing.assert_array_equal(expected, np.column_stack([actual['tick'], actual['neuron_idx']]))
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,197 @@
"""Synthetic sparse-event updates only; no recorded trajectories or training."""
import copy
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import tempfile
import unittest
import numpy as np
import conditioning_update as m
def source_path():
# Same PHASE4J_SOURCE and ordered local candidates as frozen AM tests.
explicit = os.environ.get("PHASE4J_SOURCE")
if explicit:
return Path(explicit)
parent = Path(__file__).resolve().parents[1]
paths = (parent / "models/male-cns-lif/phase4j.py", parent / "mb-physiology-v1/phase4j.py")
for path in paths:
if path.is_file():
return path
raise FileNotFoundError("Set PHASE4J_SOURCE to the SHA-pinned original phase4j.py")
SOURCE = source_path()
BRIDGE = Path(os.environ.get("AM_ELIGIBILITY_BRIDGE",
str(Path(__file__).resolve().parents[1] / "wholebrain-plastic-interface-v1/eligibility_bridge.py")))
def args(rows=((0, 1), (3, 3)), us_tick=3, steps=8):
events = np.asarray(rows, np.int32).reshape(-1, 2)
return dict(step=events[:, 0], neuron_idx=events[:, 1], kc=np.array([1, 3, 7], np.int32),
candidate_edge_id=np.array([1, 4, 5, 10, 12], np.int64),
candidate_sources=np.array([1, 1, 3, 7, 3], np.int32),
candidate_targets=np.array([2, 2, 5, 5, 9], np.int32),
candidate_compartments=np.array(["y5B'2a", "a1", "y5B'2a", "y5B'2a", "y2a'1"]),
delta_before=np.zeros(5, np.float32), base_candidate=np.array([.275, .55, .0001, .275, 2.75], np.float32),
steps=steps, n_neurons=10, compartment="y5B'2a", us_tick=us_tick,
d_command=1.0 if us_tick is not None else 0.0, frozen=False,
bridge_source=BRIDGE, phase4j_source=SOURCE, trace_backend="numpy")
class ConditioningUpdate(unittest.TestCase):
def test_pinned_original_calls_and_recomputable_receipt(self):
inputs = args()
result = m.update_epoch(**inputs)
bridge = m._load_bridge(BRIDGE)
traced = bridge.trace_from_events(inputs["step"], inputs["neuron_idx"], inputs["kc"], 8, [3],
phase4j_source=SOURCE, backend="numpy", n_neurons=10)
expected, _ = bridge.apply_eligibility(traced.last_command_trace, inputs["kc"],
inputs["candidate_sources"], inputs["candidate_compartments"], inputs["compartment"],
inputs["delta_before"], inputs["base_candidate"], d=1., phase4j_source=SOURCE, n_neurons=10)
self.assertTrue(bridge.bit_equal(result.delta_after, expected))
self.assertTrue(m.verify_receipt(result.receipt, **inputs))
self.assertEqual(result.receipt["source_pins"]["eligibility_bridge_sha256"], hashlib.sha256(BRIDGE.read_bytes()).hexdigest())
self.assertEqual(result.receipt["trace_audit"]["sampling"], "after_current_tick_spike")
json.dumps(result.receipt, allow_nan=False)
def test_zero_d_and_frozen_preserve_existing_delta(self):
for d, frozen in ((0., False), (1., True), (0., True)):
inputs = args()
inputs["delta_before"][:] = -.25 * inputs["base_candidate"]
inputs.update(d_command=d, frozen=frozen)
result = m.update_epoch(**inputs)
self.assertEqual(result.delta_after.tobytes(), inputs["delta_before"].tobytes())
self.assertEqual(result.receipt["counts"]["original_updated_edges_attempted"], 0)
self.assertEqual(result.receipt["counts"]["actual_bitwise_changed_edges"], 0)
def test_us_before_cs_does_not_use_future_trace(self):
result = m.update_epoch(**args(rows=((2, 1),), us_tick=1))
self.assertFalse(np.any(result.sampled_trace))
self.assertTrue(np.any(result.final_trace))
self.assertFalse(np.any(result.delta_after))
def test_current_tick_and_reentrant_kc_saturate_before_sample(self):
result = m.update_epoch(**args(rows=((0, 1), (1, 1), (3, 1), (3, 3)), us_tick=3))
self.assertEqual(result.sampled_trace.tolist(), [1., 1., 0.])
self.assertEqual(result.delta_after[0].view(np.uint32), np.float32(-.00275).view(np.uint32))
self.assertEqual(result.receipt["counts"]["actual_KC_events"], 4)
def test_non_kc_only_changes_event_evidence_not_update(self):
a = m.update_epoch(**args(rows=((0, 1), (3, 3))))
b = m.update_epoch(**args(rows=((0, 0), (0, 1), (2, 9), (3, 3))))
self.assertEqual(a.delta_after.tobytes(), b.delta_after.tobytes())
self.assertEqual(a.sampled_trace.tobytes(), b.sampled_trace.tobytes())
self.assertNotEqual(a.receipt["input_arrays"], b.receipt["input_arrays"])
def test_no_us_uses_zero_not_final_trace(self):
result = m.update_epoch(**args(us_tick=None))
self.assertFalse(np.any(result.sampled_trace))
self.assertFalse(np.any(np.signbit(result.sampled_trace)))
self.assertTrue(np.any(result.final_trace))
self.assertEqual(result.receipt["counts"]["actual_value_changed_edges"], 0)
def test_attempts_and_fully_saturated_changes_are_distinct(self):
inputs = args(rows=((3, 1), (3, 3), (3, 7)))
inputs["delta_before"][:] = -.25 * inputs["base_candidate"]
result = m.update_epoch(**inputs)
counts = result.receipt["counts"]
self.assertEqual(counts["original_updated_edges_attempted"], 3)
self.assertEqual(counts["actual_value_changed_edges"], 0)
self.assertEqual(counts["selected_at_floor_before"], 3)
self.assertEqual(counts["selected_at_floor_after"], 3)
self.assertEqual(counts["selected_newly_at_floor"], 0)
def test_new_floor_and_off_compartment_identity(self):
inputs = args()
inputs["delta_before"][1] = -.01
result = m.update_epoch(**inputs)
self.assertEqual(result.receipt["counts"]["selected_newly_at_floor"], 1)
self.assertEqual(result.receipt["counts"]["actual_bitwise_changed_edges"], 2)
self.assertEqual(result.delta_after[[1, 3, 4]].tobytes(), inputs["delta_before"][[1, 3, 4]].tobytes())
def test_owned_readonly_results_and_all_input_bytes_unchanged(self):
inputs = args()
before = {k: v.tobytes() for k, v in inputs.items() if isinstance(v, np.ndarray)}
result = m.update_epoch(**inputs)
for key, raw in before.items():
self.assertEqual(inputs[key].tobytes(), raw)
for output in (result.delta_after, result.sampled_trace, result.final_trace):
self.assertTrue(output.flags.owndata)
self.assertFalse(output.flags.writeable)
self.assertTrue(all(not np.shares_memory(output, inputs[key]) for key in before))
inputs["delta_before"][:] = -.000001
self.assertNotEqual(result.delta_after.tobytes(), inputs["delta_before"].tobytes())
def test_scope_claims_do_not_prove_graph_or_dan_delivery(self):
receipt = m.update_epoch(**args()).receipt
for field in ("complete_KC_identity_verified", "complete_mask_identity_verified", "full_graph_authority_verified",
"brain_dynamics_executed", "weights_installed", "base_modified", "training_executed_by_this_function",
"scientific_learning_pass_claimed", "candidate_generated", "numpy_torch_bitwise_comparison_performed",
"frozen_Torch_expression_executed"):
self.assertIs(receipt[field], False)
self.assertFalse(receipt["semantics"]["actual_DAN_delivery_observed"])
self.assertFalse(receipt["semantics"]["actual_DAN_delivery_used_to_gate"])
def test_invalid_events_mapping_commands_and_values_fail_closed(self):
mutations = (
("step", np.array([0, 3], np.int64)), ("steps", True), ("us_tick", False),
("d_command", True), ("d_command", float("nan")), ("frozen", 1), ("trace_backend", "auto"),
("candidate_edge_id", np.array([1, 1, 5, 10, 12], np.int64)),
("candidate_targets", np.array([2, 2, 5, 5, 10], np.int32)),
("candidate_sources", np.array([0, 1, 3, 7, 3], np.int32)),
("base_candidate", np.array([.275, .55, float("inf"), .275, 2.75], np.float32)),
("delta_before", np.full(5, -0., np.float32)),
("kc", np.array([1, 1, 7], np.int32)),
)
for key, value in mutations:
inputs = args()
inputs[key] = value
with self.subTest(key=key), self.assertRaises(ValueError):
m.update_epoch(**inputs)
for rows in (((0, 1), (0, 1)), ((3, 3), (0, 1)), ((8, 1),)):
with self.assertRaises(ValueError):
m.update_epoch(**args(rows=rows))
inputs = args(us_tick=None)
inputs["d_command"] = 1
with self.assertRaises(ValueError):
m.update_epoch(**inputs)
def test_receipt_forgery_fails_after_new_self_hash(self):
inputs = args()
bad = copy.deepcopy(m.update_epoch(**inputs).receipt)
bad["counts"]["actual_value_changed_edges"] = 99
bad["payload_sha256"] = m._sha({k: v for k, v in bad.items() if k != "payload_sha256"})
with self.assertRaisesRegex(ValueError, "exact recomputation"):
m.verify_receipt(bad, **inputs)
def test_source_drift_rejected_before_loading(self):
with tempfile.TemporaryDirectory() as directory:
for field, source in (("bridge_source", BRIDGE), ("phase4j_source", SOURCE)):
path = Path(directory) / (field + ".py")
path.write_bytes(source.read_bytes() + b"\nraise RuntimeError('never execute')\n")
inputs = args()
inputs[field] = path
with self.subTest(field=field), self.assertRaisesRegex(ValueError, "SHA256 mismatch"):
m.update_epoch(**inputs)
@unittest.skipUnless(importlib.util.find_spec("torch") is not None,
"Torch absent: per-call NumPy/Torch exact compatibility NOT established locally")
def test_frozen_torch_cpu_matches_numpy_on_synthetic_events(self):
inputs = args(rows=((0, 1), (1, 1), (3, 3), (7, 7)), us_tick=7)
numpy_result = m.update_epoch(**inputs)
inputs["trace_backend"] = "torch"
torch_result = m.update_epoch(**inputs)
for field in ("delta_after", "sampled_trace", "final_trace"):
self.assertEqual(getattr(numpy_result, field).tobytes(), getattr(torch_result, field).tobytes())
self.assertTrue(torch_result.receipt["frozen_Torch_expression_executed"])
self.assertFalse(torch_result.receipt["numpy_torch_bitwise_comparison_performed"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,195 @@
"""Synthetic planning tests only; no input generation, files or brain runs."""
import copy
from collections import Counter
import unittest
from unittest.mock import patch
import evaluation_plan as m
import trial_plan
def synthetic_config(trial, reversal_points=(0, 50)):
"""An explicit TEST choice, not a proposed production default.
Controls here use a simple fixed epoch fraction; the planner never supplies
this policy. Only paired/reversal checkpoints track actual scheduled USs.
"""
config = dict(schema=m.CONFIG_SCHEMA, probe_steps=1200, cs_start_tick=0,
cs_stop_tick_exclusive=1000, reversal_checkpoints=list(reversal_points),
baseline_execution="execute_each_snapshot", snapshot_binding_policy="explicit_completed_epochs",
snapshot_bindings=[])
for block in trial["plan"]["blocks"]:
for suite in block["suites"]:
points = list(reversal_points) if suite["assay"] == "reversal" else block["evaluation_checkpoints"]
for arm in suite["arms"]:
for point in points:
if point in (0, 50):
completed = 0 if point == 0 else len(arm["epochs"])
elif arm["arm"] in ("paired", "reversal", "no_reversal"):
rewarded = [i + 1 for i, e in enumerate(arm["epochs"]) if e["us"]["requested"]]
completed = rewarded[point - 1]
else:
completed = point * len(arm["epochs"]) // 50
config["snapshot_bindings"].append(dict(block_id=block["block_id"], assay=suite["assay"],
arm=arm["arm"], checkpoint=point, completed_epochs=completed))
return config
class EvaluationPlan(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Existing synthetic master from the trial-plan tests, not a real choice.
cls.trial = trial_plan.build_trial_plan(461)
cls.config = synthetic_config(cls.trial)
cls.plan = m.build_evaluation_plan(cls.trial, cls.config)
def test_public_full_authority_reconstruction(self):
self.assertTrue(m.validate_evaluation_plan(self.plan, self.trial, self.config))
def test_exact_inventory_budget_and_no_execution_claim(self):
b = self.plan["budget"]
self.assertEqual(b["probes_by_assay"], dict(normal=3920, delayed=3920, reversal=320, retention=480))
self.assertEqual(b["evaluation_probes"], 8640)
self.assertEqual(b["snapshot_count"], 4320)
self.assertEqual(b["evaluation_steps"], 10368000)
self.assertEqual(b["planned_scientific_steps_including_evaluation"], 117968000)
self.assertEqual(b["independent_evaluation_inputs"], 80)
for name in ("execution_authorized", "training_executed", "inputs_generated", "candidate_generated",
"learning_pass_claimed", "scientific_gate_added", "external_source_and_snapshot_authority_verified"):
self.assertIs(self.plan[name], False)
def test_full_cells_and_all_control_coverage(self):
cells = {}
for p in self.plan["probes"]:
cells.setdefault((p["mode"], p["dan_type"], p["original_reinforced_cue"]), set()).add(p["training_replicate"])
self.assertEqual(p["odor"], trial_plan.ODORS[p["cue"]])
self.assertFalse(p["weight_updates_enabled"])
self.assertFalse(p["US_requested"])
self.assertFalse(p["feedback_to_training_or_idle"])
self.assertFalse(p["adds_training_replicate"])
self.assertEqual(len(cells), 8)
self.assertTrue(all(v == {1, 2, 3, 4, 5} for v in cells.values()))
first = self.trial["plan"]["blocks"][0]["block_id"]
normal = [p for p in self.plan["probes"] if p["block_id"] == first and p["assay"] == "normal"]
self.assertEqual(Counter(p["arm"] for p in normal), Counter({arm: 14 for arm in trial_plan.ARMS}))
def test_stream_keys_disjoint_and_reused_as_paired_probes(self):
non_eval = {r["seed"] for r in self.trial["seed_manifest"]["independent_streams"] if r["split"] != "evaluation"}
grouped = {}
for p in self.plan["probes"]:
grouped.setdefault((p["block_id"], p["cue"]), set()).add((p["input"]["seed"], p["input"]["stream_id"]))
self.assertEqual(p["input"]["semantic_key"], [p["block_id"], "evaluation", "CS", p["cue"], "00", "base-probe"])
self.assertEqual(p["input"]["seed_bits"], 63)
seeds = {next(iter(values))[0] for values in grouped.values()}
self.assertEqual(len(grouped), 80)
self.assertTrue(all(len(v) == 1 for v in grouped.values()))
self.assertEqual(len(seeds), 80)
self.assertFalse(seeds & non_eval)
self.assertTrue(any(seed > 2**32 for seed in seeds))
def test_reversal_original_baseline_and_orientation(self):
for s in self.plan["snapshots"]:
if s["assay"] == "reversal":
self.assertEqual(s["baseline_snapshot_id"], s["block_id"] + "/normal/paired/checkpoint-00")
if s["checkpoint"] == 0:
self.assertEqual(s["delta_source"], {"kind": "fork", "source": s["block_id"] + "/normal/paired/terminal-delta"})
self.assertEqual(s["neural_state_source"]["kind"], "common_frozen_reset")
def test_retention_same_idle_lineage_independent_forks(self):
grouped = {}
for s in self.plan["snapshots"]:
if s["assay"] != "retention":
continue
src = s["neural_state_source"]
grouped.setdefault((s["block_id"], s["arm"]), []).append(src)
self.assertFalse(src["reset_or_analytic_time_advance"])
self.assertFalse(s["snapshot_mutated_by_planning"])
self.assertEqual(s["baseline_snapshot_id"], s["block_id"] + "/normal/" + s["arm"] + "/checkpoint-00")
self.assertEqual(len(grouped), 80)
for rows in grouped.values():
self.assertEqual([r["elapsed_ticks"] for r in rows], [0, 10000, 100000])
self.assertEqual(len({r["idle_lineage_id"] for r in rows}), 1)
self.assertEqual(rows[0]["kind"], "actual_terminal_checkpoint")
self.assertEqual(rows[1]["kind"], "continuous_idle_checkpoint")
def test_missing_choices_no_hidden_default(self):
for key in m.CONFIG_FIELDS:
bad = copy.deepcopy(self.config)
del bad[key]
with self.subTest(key=key), self.assertRaisesRegex(ValueError, "missing="):
m.build_evaluation_plan(self.trial, bad)
self.assertIn("snapshot_bindings", m.required_choices())
def test_bad_configuration_types_and_unimplemented_reuse(self):
for key, value in (("probe_steps", 1200.0), ("cs_start_tick", False),
("cs_stop_tick_exclusive", True), ("cs_stop_tick_exclusive", 1201),
("reversal_checkpoints", [False, 50]), ("reversal_checkpoints", [50]),
("baseline_execution", "reuse_identical"), ("snapshot_binding_policy", "guess_from_paired")):
bad = copy.deepcopy(self.config)
bad[key] = value
with self.subTest(key=key, value=value), self.assertRaises(ValueError):
m._assemble(self.trial, bad)
def test_binding_coverage_order_types_and_pairing_boundary(self):
changes = (
lambda rows: rows.pop(), lambda rows: rows.reverse(),
lambda rows: rows[0].update(checkpoint=False),
lambda rows: rows[0].update(completed_epochs=False),
lambda rows: rows[1].update(completed_epochs=0),
lambda rows: rows[6].update(completed_epochs=99),
lambda rows: rows[1].update(completed_epochs=100),
lambda rows: rows[0].update(extra="unknown"),
)
for index, change in enumerate(changes):
bad = copy.deepcopy(self.config)
change(bad["snapshot_bindings"])
with self.subTest(index=index), self.assertRaises(ValueError):
m._assemble(self.trial, bad)
def test_explicit_alternative_curve_changes_budget_not_seed_inventory(self):
config = synthetic_config(self.trial, trial_plan.PROBE_CHECKPOINTS)
result = m._assemble(self.trial, config)
self.assertEqual(result["budget"]["evaluation_probes"], 9440)
self.assertEqual(result["budget"]["probes_by_assay"]["reversal"], 1120)
self.assertEqual(result["budget"]["independent_evaluation_inputs"], 80)
config["cs_stop_tick_exclusive"] = 1200
alternate = m._assemble(self.trial, config)
self.assertNotEqual(result["config_sha256"], alternate["config_sha256"])
self.assertTrue(all(p["cs_stop_tick_exclusive"] == 1200 for p in alternate["probes"]))
def test_payload_mutations_rejected_even_when_self_hash_updated(self):
changes = (
lambda p: p["probes"].pop(), lambda p: p["probes"].reverse(),
lambda p: p["probes"][0]["input"].update(seed=1),
lambda p: p["probes"][0].update(training_replicate=True),
lambda p: p["probes"][0].update(adds_training_replicate=True),
lambda p: p.update(execution_authorized=True),
)
# Full trial authority was validated in setUpClass and again above.
# Isolate evaluation reconstruction here, avoiding six repeated rebuilds
# of the large, unchanged 68k-epoch synthetic training schedule.
with patch.object(m.trial_plan, "validate_trial_plan", return_value=True):
for index, change in enumerate(changes):
bad = copy.deepcopy(self.plan)
change(bad)
bad["payload_sha256"] = m._sha({k: v for k, v in bad.items() if k != "payload_sha256"})
with self.subTest(index=index), self.assertRaises(ValueError):
m.validate_evaluation_plan(bad, self.trial, self.config)
def test_parent_authority_failure_propagates(self):
with patch.object(m.trial_plan, "validate_trial_plan", side_effect=ValueError("invalid full parent")):
with self.assertRaisesRegex(ValueError, "invalid full parent"):
m.build_evaluation_plan(self.trial, self.config)
def test_no_mutable_alias_to_caller(self):
result = m._assemble(self.trial, self.config)
result["config"]["snapshot_bindings"][0]["completed_epochs"] = 900
result["snapshots"][0]["delta_source"]["kind"] = "corruption"
result["probes"][0]["input"]["semantic_key"][0] = "corruption"
self.assertEqual(self.config["snapshot_bindings"][0]["completed_epochs"], 0)
self.assertEqual(self.trial["plan"]["blocks"][0]["suites"][0]["arms"][0]["delta_start"]["kind"], "zero")
self.assertEqual(m._assemble(self.trial, self.config), self.plan)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,262 @@
"""Synthetic plans only; no neural input generation, files, remote or execution."""
import copy
import unittest
from unittest import mock
import evaluation_plan
import evaluation_policy as policy
import trial_plan
class EvaluationPolicyTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Explicit synthetic fixture seed, never a proposed production master.
cls.trial = trial_plan.build_trial_plan(461)
cls.result = policy.build_evaluation_policy(cls.trial)
cls.registry = policy._registry_index(cls.trial)
cls.rows = cls.result["audit"]["bindings"]
def suite(self, assay="normal"):
block = self.trial["plan"]["blocks"][0]
suite = next(s for s in block["suites"] if s["assay"] == assay)
return block, copy.deepcopy(suite)
def check_suite(self, block, suite, registry=None):
return policy._suite_policy(block, suite, self.trial["plan"]["master"],
self.registry if registry is None else registry)
def test_fixed_config_and_complete_consumer_inventory(self):
config = self.result["config"]
self.assertEqual(set(config), evaluation_plan.CONFIG_FIELDS)
self.assertEqual(config["probe_steps"], 1200)
self.assertEqual(config["cs_start_tick"], 0)
self.assertEqual(config["cs_stop_tick_exclusive"], 1000)
self.assertEqual(config["reversal_checkpoints"], [0, 1, 2, 5, 10, 20, 50])
self.assertEqual(config["baseline_execution"], "execute_each_snapshot")
self.assertEqual(len(config["snapshot_bindings"]), 4480)
# The original public consumer performs its own full authority check.
inventory = evaluation_plan.build_evaluation_plan(self.trial, config)
self.assertEqual(inventory["budget"]["evaluation_probes"], 9440)
self.assertEqual(inventory["budget"]["snapshot_count"], 4720)
self.assertEqual(inventory["budget"]["evaluation_steps"], 11328000)
self.assertEqual(inventory["budget"]["planned_scientific_steps_including_evaluation"], 118928000)
self.assertEqual(inventory["budget"]["probes_by_assay"],
{"normal": 3920, "delayed": 3920, "reversal": 1120, "retention": 480})
self.assertFalse(inventory["execution_authorized"])
self.assertFalse(inventory["budget"]["baseline_output_deduplication"])
def test_each_binding_audited_in_exact_order(self):
bindings = self.result["config"]["snapshot_bindings"]
self.assertEqual([r["binding"] for r in self.rows], bindings)
identities = [(r["block_id"], r["assay"], r["arm"], r["checkpoint"]) for r in bindings]
self.assertEqual(len(identities), len(set(identities)))
self.assertEqual({r["checkpoint"] for r in bindings if r["assay"] == "reversal"}, set(policy.POINTS))
self.assertEqual(self.result["audit"]["coverage"]["snapshots_by_assay"],
{"normal": 1960, "delayed": 1960, "reversal": 560})
def test_five_independent_replicates_not_relabelled_from_probes(self):
coverage = self.result["audit"]["coverage"]
self.assertEqual(coverage["training_blocks"], 40)
self.assertEqual(coverage["training_replicates_per_cell"], 5)
self.assertEqual(coverage["new_random_streams"], 0)
self.assertFalse(coverage["new_master_seed_selected"])
self.assertEqual(self.result["seed_manifest_sha256"], self.trial["seed_manifest"]["payload_sha256"])
def test_normal_and_delayed_controls_match_actual_ordered_source_prefixes(self):
selected = [r for r in self.rows if r["binding"]["assay"] in ("normal", "delayed") and
r["binding"]["arm"] != "dan_only"]
self.assertEqual(len(selected), 3360)
for row in selected:
self.assertIs(row["ordered_CS_prefix_exact"], True)
self.assertEqual(row["actual"]["ordered_CS_source_identity_sha256"],
row["reference"]["ordered_CS_source_identity_sha256"])
self.assertEqual(row["actual"]["CS_counts_by_cue"], row["reference"]["CS_counts_by_cue"])
if row["binding"]["arm"] != "unpaired":
self.assertEqual(row["binding"]["completed_epochs"], row["reference_completed_epochs"])
self.assertTrue(row["elapsed_ticks_equal_to_reference"])
def test_unpaired_earliest_matching_prefix_and_full_terminal_including_tail(self):
any_earlier_US = False
any_trailing_US = False
for block in self.trial["plan"]["blocks"]:
for suite in block["suites"][:2]:
arms = {a["arm"]: a for a in suite["arms"]}
paired, unpaired = arms["paired"], arms["unpaired"]
rows = [r for r in self.rows if r["binding"]["block_id"] == block["block_id"] and
r["binding"]["assay"] == suite["assay"] and r["binding"]["arm"] == "unpaired"]
for row in rows:
point = row["binding"]["checkpoint"]
count = row["binding"]["completed_epochs"]
needed = row["reference_completed_epochs"]
if point == 0:
self.assertEqual(count, 0)
elif point == 50:
self.assertEqual(count, 150)
self.assertEqual(row["actual"]["CS_epochs"], 100)
self.assertEqual(row["actual"]["requested_US_epochs"], 50)
self.assertEqual(row["elapsed_ticks_difference"], 50 * paired["epochs"][0]["steps"])
any_trailing_US |= unpaired["epochs"][-1]["cs"] is None
else:
actual_positions = [i + 1 for i, e in enumerate(unpaired["epochs"]) if e["cs"] is not None]
self.assertEqual(count, actual_positions[needed - 1])
self.assertIsNotNone(unpaired["epochs"][count - 1]["cs"])
self.assertEqual(row["actual"]["requested_US_epochs"], count - needed)
any_earlier_US |= count > needed
self.assertTrue(any_earlier_US)
self.assertTrue(any_trailing_US)
def test_DAN_only_is_yoked_zero_CS_not_equal_exposure(self):
rows = [r for r in self.rows if r["binding"]["arm"] == "dan_only"]
self.assertEqual(len(rows), 560)
for row in rows:
self.assertEqual(row["actual"]["CS_epochs"], 0)
self.assertIsNone(row["ordered_CS_prefix_exact"])
self.assertFalse(row["CS_exposure_equal"])
self.assertTrue(row["DAN_only_yoke_verified"])
self.assertTrue(row["requested_US_count_equal_to_reference"])
self.assertTrue(row["elapsed_ticks_equal_to_reference"])
self.assertEqual(row["binding"]["completed_epochs"], row["reference_completed_epochs"])
def test_reversal_each_own_US_boundary_and_original_terminal(self):
saw_different_middle_CS_prefix = False
for block in self.trial["plan"]["blocks"]:
suite = block["suites"][2]
arms = {a["arm"]: a for a in suite["arms"]}
for row in [r for r in self.rows if r["binding"]["block_id"] == block["block_id"] and
r["binding"]["assay"] == "reversal"]:
binding = row["binding"]
arm = arms[binding["arm"]]
point, count = binding["checkpoint"], binding["completed_epochs"]
if point in (0, 50):
self.assertEqual(count, 0 if point == 0 else 100)
self.assertTrue(row["ordered_CS_prefix_exact"])
else:
ordinal_positions = [i + 1 for i, e in enumerate(arm["epochs"]) if e["us"]["requested"]]
self.assertEqual(count, ordinal_positions[point - 1])
self.assertTrue(arm["epochs"][count - 1]["us"]["requested"])
saw_different_middle_CS_prefix |= not row["ordered_CS_prefix_exact"]
self.assertEqual(row["actual"]["requested_US_epochs"], point)
self.assertEqual(row["actual"]["enabled_nonzero_D_update_opportunities"], point)
self.assertTrue(saw_different_middle_CS_prefix)
def test_intermediate_US_time_and_update_mismatches_are_explicit_not_failed(self):
middle = [r for r in self.rows if r["binding"]["checkpoint"] not in (0, 50)]
self.assertTrue(any(r["binding"]["arm"] == "random_reward" and r["requested_US_count_difference"] != 0 for r in middle))
self.assertTrue(any(r["binding"]["arm"] == "unpaired" and r["elapsed_ticks_difference"] > 0 for r in middle))
self.assertTrue(any(r["binding"]["arm"] == "frozen" and r["nonzero_D_update_opportunity_difference"] < 0 for r in middle))
self.assertTrue(all(r["all_US_timing_or_total_exposure_matched_claimed"] is False for r in self.rows))
self.assertTrue(all(r["actual"]["actual_accepted_US_or_DAN_spikes_known"] is False for r in self.rows))
def test_all_boundaries_follow_complete_epoch_update_not_US_tick(self):
for row in self.rows:
self.assertEqual(row["boundary"], "after complete epoch and scheduled delta update; not at the US tick")
steps = 1800 if row["binding"]["assay"] == "delayed" else 1200
self.assertEqual(row["actual"]["elapsed_training_ticks"], row["binding"]["completed_epochs"] * steps)
self.assertEqual(row["terminal_includes_all_epochs"], row["binding"]["checkpoint"] == 50)
def test_control_seed_or_source_alias_mutations_fail_without_relying_on_counts(self):
for field, change in (("seed", lambda v: v + 1),
("source_stream_id", lambda v: v + "x"),
("stream_id", lambda v: v + "x"),
("start_tick", lambda v: False)):
with self.subTest(field=field):
block, suite = self.suite()
cs = suite["arms"][1]["epochs"][0]["cs"]
cs[field] = change(cs[field])
with self.assertRaises(ValueError):
self.check_suite(block, suite)
def test_reordered_cues_with_same_counts_and_valid_individual_aliases_fail(self):
block, suite = self.suite()
epochs = suite["arms"][1]["epochs"]
index = next(i for i in range(1, len(epochs)) if epochs[i]["cs"]["cue"] != epochs[0]["cs"]["cue"])
epochs[0]["cs"], epochs[index]["cs"] = epochs[index]["cs"], epochs[0]["cs"]
with self.assertRaisesRegex(ValueError, "CS occurrence order|ordered CS|CS sequence"):
self.check_suite(block, suite)
def test_corrupt_alias_registry_is_not_excused_by_equal_CS_seed(self):
block, suite = self.suite()
independent, aliases = self.registry
changed = copy.deepcopy(aliases)
cs = suite["arms"][1]["epochs"][0]["cs"]
key = (block["block_id"], "normal", "CS", cs["cue"], f'{cs["occurrence"]:02d}', "d_zero")
changed[("training", key)]["source_key"][-1] = "frozen"
with self.assertRaisesRegex(ValueError, "alias is not registered"):
self.check_suite(block, suite, (independent, changed))
def test_DAN_yoked_cue_US_or_added_CS_mutations_fail(self):
for mutation in ("cue", "US", "CS"):
with self.subTest(mutation=mutation):
block, suite = self.suite()
paired = suite["arms"][0]
dan = suite["arms"][-1]
epoch = dan["epochs"][0]
if mutation == "cue":
epoch["yoked_cue_removed"] = "B" if epoch["yoked_cue_removed"] == "A" else "A"
elif mutation == "US":
epoch["us"]["nominal_slot_tick"] += 1
else:
epoch["cs"] = copy.deepcopy(paired["epochs"][0]["cs"])
with self.assertRaises(ValueError):
self.check_suite(block, suite)
def test_missing_reordered_or_duplicate_control_arms_fail(self):
for mutation in ("missing", "reorder", "duplicate"):
with self.subTest(mutation=mutation):
block, suite = self.suite()
if mutation == "missing":
suite["arms"].pop()
elif mutation == "reorder":
suite["arms"][0], suite["arms"][1] = suite["arms"][1], suite["arms"][0]
else:
suite["arms"].append(suite["arms"][0])
with self.assertRaisesRegex(ValueError, "control arms"):
self.check_suite(block, suite)
def test_result_not_aliased_no_execution_or_new_scientific_gate(self):
fields = ("protocol_approval_claimed", "execution_authorized", "training_executed", "inputs_generated",
"input_generator_implementation_verified", "source_and_snapshot_authority_verified",
"candidate_generated", "learning_pass_claimed", "scientific_gate_added")
self.assertTrue(all(self.result[field] is False for field in fields))
self.assertEqual(self.result["status"], "DRAFT-NOT-REGISTERED")
self.assertTrue(self.result["choices_are_new_draft_proposals_not_inherited_gates"])
self.assertTrue(self.result["audit"]["unchanged_retention_scope"]["same_continuous_ten_second_lineage"])
self.assertFalse(self.result["audit"]["unchanged_retention_scope"]["retained_snapshot_execution_or_lineage_verified"])
# Config and audit may describe the same row, but neither is a caller alias.
block = self.trial["plan"]["blocks"][0]
self.assertIsNot(self.result["config"]["reversal_checkpoints"], block["evaluation_checkpoints"])
self.assertEqual(self.result["trial_payload_sha256"], self.trial["payload_sha256"])
def test_validation_rejects_rehashed_config_type_and_audit_mutations(self):
# Separate reconstruction is supplied by the already independently built
# fixture to keep mutation tests small; public full reconstruction is
# exercised once below, not hidden behind this mocked trust boundary.
with mock.patch.object(policy, "build_evaluation_policy", return_value=self.result):
for mutation in ("point", "type", "audit", "authorization"):
with self.subTest(mutation=mutation):
changed = copy.deepcopy(self.result)
if mutation == "point":
changed["config"]["snapshot_bindings"][1]["completed_epochs"] += 1
elif mutation == "type":
changed["config"]["cs_start_tick"] = False
elif mutation == "audit":
changed["audit"]["bindings"][0]["actual"]["CS_epochs"] += 1
else:
changed["execution_authorized"] = True
changed["payload_sha256"] = policy._sha({k: v for k, v in changed.items() if k != "payload_sha256"})
with self.assertRaisesRegex(ValueError, "exact reconstruction"):
policy.validate_evaluation_policy(changed, self.trial)
def test_public_validation_full_reconstruction(self):
self.assertTrue(policy.validate_evaluation_policy(self.result, self.trial))
def test_trial_authority_hash_rejected_before_planning(self):
changed = dict(self.trial)
changed["payload_sha256"] = "0" * 64
with self.assertRaisesRegex(ValueError, "payload SHA"):
policy.build_evaluation_policy(changed)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,318 @@
"""Synthetic arrays/declared receipts only; no real recordings or science run."""
import copy
import hashlib
import json
import unittest
import numpy as np
import metrics as m
NODES = np.arange(6, dtype=np.int32)
POPS = {'MBON': np.array([0, 1, 2], np.int32), 'legacyDownstream': np.arange(6, dtype=np.int32),
'nonMBONDownstream': np.array([3, 4, 5], np.int32),
'MBON01': np.array([0], np.int32), 'MBON12': np.array([1], np.int32)}
def state(counts=(1, 1, 0, 1, 0, 0), *, times=None, g=None):
events = [(10 + 20 * k if times is None else times[i][k], i) for i, count in enumerate(counts) for k in range(count)]
events.sort()
return dict(step=np.array([t for t, _ in events], np.int32),
neuron_idx=np.array([i for _, i in events], np.int32), counts=np.array(counts, np.int32),
v=np.full(6, -.052, np.float64), g=np.zeros(6, np.float64) if g is None else np.array(g, np.float64),
ref=np.zeros(6, np.int32))
def effect(*, direction=1, gain=True, g_effect=True, conditioned=m.CUES[0], neutral_g=0.):
pre = {cue: state() for cue in m.CUES}
post = {cue: state() for cue in m.CUES}
counts = [1, 1, 0, 1, 0, 0]
counts[1 if direction > 0 else 0] += abs(direction)
if gain:
counts[3] += abs(direction)
g = np.zeros(6, np.float64)
if g_effect:
g[0] = .002
post[conditioned] = state(counts, g=g)
neutral = next(c for c in m.CUES if c != conditioned)
post[neutral]['g'][0] = neutral_g
return m.arm_metrics(pre, post, conditioned_cue=conditioned, nodes=NODES, populations=POPS)
def inventory(*, assay='normal', dan='PAM15', paired=None):
registration = {f'rep{i}': [f'training-source-{i}'] for i in range(5)}
rows = []
for i in range(5):
arms = {}
for name in m.ARMS:
metric = effect(direction=0, g_effect=False) if name != 'paired' else (
effect(direction=1 if dan == 'PAM15' else -1) if paired is None else copy.deepcopy(paired[i]))
evidence = dict(complete=True, recording_verified=True,
receipt_sha256=hashlib.sha256(f'{i}:{name}:{assay}'.encode()).hexdigest(), assay=assay,
epoch_duration_ms=120. if assay == 'normal' else 180., cs_duration_ms=100.,
us_tick=900 if assay == 'normal' else 1500, completed_epochs=150 if name == 'unpaired' else 100,
reinforcement_opportunities=0 if name == 'CS-only' else 50, terminal_pairings=50)
arms[name] = dict(metrics=metric, execution=evidence)
rows.append(dict(replicate_id=f'rep{i}', replicate_unit='independent_training',
training_stream_ids=registration[f'rep{i}'], evaluation_stream_ids=[f'evaluation-source-{i}'],
assay=assay, mode='physiological', dan_type=dan, conditioned_cue=m.CUES[0], arms=arms))
return rows, registration
def evaluate(rows, registration, *, assay='normal', dan='PAM15'):
return m.evaluate_acquisition(rows, assay=assay, mode='physiological', dan_type=dan,
conditioned_cue=m.CUES[0], registered_training_streams=registration)
class StateTests(unittest.TestCase):
def test_every_key_validated_and_no_input_mutation(self):
a, b = state(), state((1, 2, 0, 2, 0, 0), g=[.001, -.004, 0, 0, 0, 0])
before = {key: value.tobytes() for key, value in a.items()}
r = m.functional_summary(a, b, nodes=NODES, populations=POPS, steps=1200)
self.assertEqual(r['MBON_final_delta_g_max_abs'], .004)
self.assertEqual(r['MBON_final_delta_g_units'], 'SI volts')
self.assertIn('not time peak', r['MBON_final_delta_g_scope'])
self.assertEqual(r['wholebrain']['spike_count_l1'], 2)
self.assertEqual(before, {key: value.tobytes() for key, value in a.items()})
json.dumps(r, allow_nan=False)
def test_first_event_minimum_not_last_and_zero_tick_is_defined(self):
a = state((3, 0, 0, 0, 0, 0), times=[[0, 20, 100], [], [], [], [], []])
r = m.validate_state(a, nodes=NODES, steps=1200)
self.assertEqual(r['first_tick'].tolist(), [0, -1, -1, -1, -1, -1])
def test_missing_latencies_never_zero_filled(self):
a, b = state((0, 0, 0, 0, 0, 0)), state((1, 0, 0, 0, 0, 0))
r = m.functional_summary(a, b, nodes=NODES, populations=POPS, steps=1200)['populations']['MBON']
self.assertIsNone(r['pre_first_latency_ms'])
self.assertIsNone(r['first_latency_delta_ms'])
self.assertFalse(r['defined_first_latency_changed'])
self.assertEqual(r['first_latency_became_defined_neurons'], 1)
self.assertGreater(r['spike_count_l1'], 0)
def test_nonfinite_input_all_neurons_not_only_scored_targets(self):
for key in ('v', 'g'):
for value in (np.nan, np.inf, -np.inf):
with self.subTest(key=key, value=value):
a = state(); a[key][5] = value
with self.assertRaisesRegex(ValueError, 'Nonfinite'):
m.functional_summary(a, state(), nodes=NODES, populations=POPS, steps=1200)
def test_nonfinite_derived_difference_rejected(self):
a, b = state(), state()
a['g'][0], b['g'][0] = -1e308, 1e308
with self.assertRaisesRegex(ValueError, 'Nonfinite derived'):
m.functional_summary(a, b, nodes=NODES, populations=POPS, steps=1200)
def test_bad_state_dtype_shape_counts_order_bounds_rejected(self):
variants = []
a = state(); a['counts'] = a['counts'].astype(np.int64); variants.append(a)
a = state(); a['counts'][0] += 1; variants.append(a)
a = state(); a['step'][0] = -1; variants.append(a)
a = state(); a['neuron_idx'][0] = 99; variants.append(a)
a = state(); a['ref'][0] = 23; variants.append(a)
a = state(); a['v'] = a['v'][:-1]; variants.append(a)
a = state(); a['neuron_idx'][1] = 0; variants.append(a)
a = state(); a['extra'] = np.zeros(1); variants.append(a)
for a in variants:
with self.subTest(keys=list(a)), self.assertRaises(ValueError):
m.validate_state(a, nodes=NODES, steps=1200)
def test_empty_wrong_duplicate_and_overlapping_memberships_rejected(self):
for key, value in [('MBON12', []), ('MBON01', [0, 0]), ('MBON12', [3]),
('MBON12', [0]), ('nonMBONDownstream', [2, 3]), ('MBON', [0, 1, 99]),
('nonMBONDownstream', [3, 4]), ('legacyDownstream', [0, 1, 3, 4, 5])]:
populations = dict(POPS, **{key: np.array(value, np.int32)})
with self.subTest(key=key, value=value), self.assertRaises(ValueError):
m.validate_populations(NODES, populations)
def test_legacy_downstream_retains_mbon_and_new_setdiff_is_explicit(self):
result = m.validate_populations(NODES, POPS)
self.assertEqual(result['indices']['legacyDownstream'], [0, 1, 2, 3, 4, 5])
self.assertEqual(result['indices']['nonMBONDownstream'], [3, 4, 5])
self.assertIn('NEW hypothesis', result['population_semantics']['nonMBONDownstream'])
old_alias = dict(POPS)
old_alias['trueDownstream'] = old_alias.pop('nonMBONDownstream')
with self.assertRaisesRegex(ValueError, 'exactly'):
m.validate_populations(NODES, old_alias)
class ContrastTests(unittest.TestCase):
def test_fixed_types_not_mask_and_type_mean_not_population_sum(self):
p = dict(POPS, MBON=np.array([0, 1, 2], np.int32), MBON01=np.array([0], np.int32), MBON12=np.array([1, 2], np.int32))
pre = {c: state() for c in m.CUES}
post = {c: state() for c in m.CUES}; post[m.CUES[0]] = state((1, 3, 1, 2, 0, 0))
r = m.arm_metrics(pre, post, conditioned_cue=m.CUES[0], nodes=NODES, populations=p)
self.assertEqual(r['rawB']['post'][m.CUES[0]], 1.)
self.assertEqual(r['rawS'], 1.5)
self.assertFalse(r['B_is_decoder'])
def test_reciprocal_role_keeps_signed_orientation(self):
first, second = effect(), effect(conditioned=m.CUES[1])
self.assertEqual(first['rawS'], second['rawS'])
self.assertEqual(second['conditioned_cue'], m.CUES[1])
def test_raw_labels_not_aliases(self):
with self.assertRaises(ValueError):
m.arm_metrics({'A': state()}, {'B': state()}, conditioned_cue='A', nodes=NODES, populations=POPS)
def test_legacy_and_non_mbon_gains_are_separate_not_an_alias(self):
result = effect(gain=False)
self.assertEqual(result['separation']['MBON']['gain'], 1.)
self.assertEqual(result['separation']['legacyDownstream']['gain'], 1.)
self.assertEqual(result['separation']['nonMBONDownstream']['gain'], 0.)
pre = {cue: state((4, 0, 0, 0, 0, 0) if cue == m.CUES[0] else (0, 0, 0, 0, 0, 0)) for cue in m.CUES}
post = {cue: state((3, 0, 0, 1, 0, 0) if cue == m.CUES[0] else (0, 0, 0, 0, 0, 0)) for cue in m.CUES}
result = m.arm_metrics(pre, post, conditioned_cue=m.CUES[0], nodes=NODES, populations=POPS)
self.assertLess(result['separation']['legacyDownstream']['gain'], 0.)
self.assertGreater(result['separation']['nonMBONDownstream']['gain'], 0.)
class GateTests(unittest.TestCase):
def test_both_directions_and_suites_pass_only_as_draft_proposal(self):
for assay in ('normal', 'delayed'):
for dan in m.DAN_TYPES:
with self.subTest(assay=assay, dan=dan):
rows, registration = inventory(assay=assay, dan=dan)
r = evaluate(rows, registration, assay=assay, dan=dan)
self.assertTrue(r['proposal_gate_pass'])
self.assertEqual(r['actual_control_receipts'], 30)
self.assertEqual(r['successful_same_replicate_conjunctions'], 5)
self.assertFalse(r['scientific_learning_pass_claimed'])
self.assertFalse(r['candidate_generated'])
self.assertTrue(all(v['rawS'] < 0 for v in r['replicates']) if dan == 'PPL103' else
all(v['rawS'] > 0 for v in r['replicates']))
json.dumps(r, allow_nan=False)
def test_opposite_or_zero_signed_effect_is_failure_not_abs_success(self):
for direction in (-1, 0):
rows, registry = inventory(paired=[effect(direction=direction)] * 5)
r = evaluate(rows, registry)
self.assertFalse(r['proposal_gate_pass'])
self.assertEqual(r['successful_same_replicate_conjunctions'], 0)
def test_equal_control_is_failure(self):
rows, registry = inventory()
for row in rows:
row['arms']['CS-only']['metrics'] = copy.deepcopy(row['arms']['paired']['metrics'])
self.assertFalse(evaluate(rows, registry)['proposal_gate_pass'])
def test_same_replicate_conjunction_not_majority_of_each_predicate(self):
effects = [effect(), effect(), effect(gain=False), effect(g_effect=False), effect(direction=-1)]
rows, registry = inventory(paired=effects)
r = evaluate(rows, registry)
self.assertEqual(r['successful_same_replicate_conjunctions'], 2)
self.assertFalse(r['proposal_gate_pass'])
def test_exact_three_of_five_pass(self):
rows, registry = inventory(paired=[effect()] * 3 + [effect(direction=0)] * 2)
self.assertTrue(evaluate(rows, registry)['proposal_gate_pass'])
def test_positive_legacy_gain_cannot_rescue_new_non_mbon_gate(self):
rows, registry = inventory(paired=[effect(gain=False)] * 5)
result = evaluate(rows, registry)
self.assertFalse(result['proposal_gate_pass'])
for row in result['replicates']:
self.assertTrue(row['predicates']['legacyDownstream_AB_euclidean_gain_positive'])
self.assertFalse(row['predicates']['nonMBONDownstream_AB_euclidean_gain_positive'])
self.assertEqual(row['population_separation_gains']['legacyDownstream'], 1.)
self.assertEqual(row['population_separation_gains']['nonMBONDownstream'], 0.)
self.assertEqual(row['legacy_audit']['H_like_downstream_population'], 'legacyDownstream')
self.assertFalse(row['new_hypothesis_audit']['inherited_from_old_H'])
def test_population_metadata_or_overlapping_raw_vectors_cannot_be_forged(self):
for mutation in ('semantics', 'setdiff', 'subset_final'):
rows, registry = inventory()
summary = rows[0]['arms']['paired']['metrics']['cues'][m.CUES[0]]
if mutation == 'semantics':
summary['membership']['population_semantics']['nonMBONDownstream'] = 'old H population'
elif mutation == 'setdiff':
ids = summary['membership']['indices']
ids['nonMBONDownstream'] = [2, 3, 4]
summary['membership']['membership_sha256'] = m._json_hash(ids)
summary['populations']['nonMBONDownstream']['neuron_idx'] = [2, 3, 4]
else:
# Self-consistent terminal deltas can still contradict the full parent population.
child = summary['populations']['nonMBONDownstream']
child['pre_final_v'][0] += .001
child['post_final_v'][0] += .001
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
evaluate(rows, registry)
def test_missing_control_incomplete_receipt_and_wrong_delayed_schedule_rejected(self):
for mutation in ('missing', 'incomplete', 'timing', 'epochs', 'duplicate_receipt', 'best_checkpoint'):
rows, registry = inventory()
if mutation == 'missing':
rows[0]['arms'].pop('unpaired')
else:
e = rows[0]['arms']['unpaired']['execution']
if mutation == 'incomplete': e['complete'] = False
if mutation == 'timing': e['us_tick'] = 1500
if mutation == 'epochs': e['completed_epochs'] = 100
if mutation == 'duplicate_receipt': e['receipt_sha256'] = rows[0]['arms']['paired']['execution']['receipt_sha256']
if mutation == 'best_checkpoint': e['terminal_pairings'] = 20
with self.subTest(mutation=mutation), self.assertRaises(ValueError): evaluate(rows, registry)
def test_evaluation_draws_cannot_be_training_replicates(self):
for mutation in ('unit', 'duplicate_id', 'training_overlap', 'eval_training_overlap', 'eval_overlap', 'extra_draw'):
rows, registry = inventory()
if mutation == 'unit': rows[0]['replicate_unit'] = 'evaluation'
if mutation == 'duplicate_id': rows[1]['replicate_id'] = rows[0]['replicate_id']
if mutation == 'training_overlap': registry['rep1'] = registry['rep0']
if mutation == 'eval_training_overlap': rows[0]['evaluation_stream_ids'] = registry['rep0']
if mutation == 'eval_overlap': rows[1]['evaluation_stream_ids'] = rows[0]['evaluation_stream_ids']
if mutation == 'extra_draw': rows.append(copy.deepcopy(rows[0]))
with self.subTest(mutation=mutation), self.assertRaises(ValueError): evaluate(rows, registry)
def test_raw_nan_forged_B_g_function_and_separation_rejected(self):
for mutation in ('nan', 'rawB', 'g', 'count', 'separation', 'latency'):
rows, registry = inventory()
metric = rows[0]['arms']['paired']['metrics']
group = metric['cues'][m.CUES[0]]['populations']['MBON']
if mutation == 'nan': metric['rawS'] = np.inf
if mutation == 'rawB': metric['rawB']['post'][m.CUES[0]] = 100.
if mutation == 'g': group['final_delta_g_max_abs'] = 100.
if mutation == 'count': group['spike_count_l1'] = 100
if mutation == 'separation': metric['separation']['MBON']['gain'] = 100.
if mutation == 'latency': group['defined_first_latency_changed'] = True
with self.subTest(mutation=mutation), self.assertRaises(ValueError): evaluate(rows, registry)
def test_control_baseline_and_cell_pooling_rejected(self):
for mutation in ('baseline', 'mode', 'DAN', 'assay', 'cue'):
rows, registry = inventory()
if mutation == 'baseline': rows[0]['arms']['frozen']['metrics']['cues'][m.CUES[0]]['pre_state_raw_sha256']['g'] = 'a' * 64
if mutation == 'mode': rows[0]['mode'] = 'matched_energy'
if mutation == 'DAN': rows[0]['dan_type'] = 'PAM-positive'
if mutation == 'assay': rows[0]['assay'] = 'delayed'
if mutation == 'cue': rows[0]['conditioned_cue'] = m.CUES[1]
with self.subTest(mutation=mutation), self.assertRaises(ValueError): evaluate(rows, registry)
def test_reversal_retention_remain_missing_not_fabricated_pass(self):
for assay in ('reversal', 'retention'):
r = m.unavailable_assay(assay)
self.assertEqual(r['status'], 'NOT-IMPLEMENTED')
self.assertIsNone(r['proposal_gate_pass'])
self.assertTrue(r['missing'])
def test_legacy_mean_control_audit_separate_and_strict_not_new_signed_gate(self):
raw = {'appetitive-conditioning': [2.] * 5, 'aversive-conditioning': [1.] * 5,
'd-zero-control': [0.] * 5, 'random-reward-control': [.5] * 5, 'frozen-brain-control': [0.] * 5}
r = m.legacy_h_mean_control_audit(raw)
self.assertTrue(r['predicate_pass'])
self.assertFalse(r['full_old_H_gate_computed'])
raw['random-reward-control'] = [1.] * 5
self.assertFalse(m.legacy_h_mean_control_audit(raw)['predicate_pass'])
raw.pop('d-zero-control')
with self.assertRaises(ValueError): m.legacy_h_mean_control_audit(raw)
def test_legacy_nan_or_evaluation_count_not_admitted(self):
raw = {'appetitive-conditioning': [2.] * 5, 'aversive-conditioning': [1.] * 5,
'd-zero-control': [0.] * 5, 'random-reward-control': [.5] * 5, 'frozen-brain-control': [0.] * 5}
for values in ([0.] * 25, [0., 0., np.nan, 0., 0.]):
raw['frozen-brain-control'] = values
with self.assertRaises(ValueError): m.legacy_h_mean_control_audit(raw)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,123 @@
import copy
import hashlib
import json
import unittest
from unittest.mock import patch
import seed_registry as m
class Seeds(unittest.TestCase):
def test_order_independent(self):
a, b = m.SeedRegistry(461), m.SeedRegistry(461)
keys = [("mode", "PAM15", str(replicate), str(epoch)) for replicate in range(5) for epoch in range(50)]
for key in keys:
a.add("training", key)
for key in reversed(keys):
b.add("training", key)
self.assertEqual(a.manifest(), b.manifest())
self.assertEqual(a.manifest()["independent_count"], 250)
self.assertTrue(m.validate_manifest(a.manifest()))
def test_splits_disjoint_and_no_sequential_overlap(self):
r = m.SeedRegistry(461)
for split in ("training", "evaluation", "design"):
for replicate in range(5):
for epoch in range(100):
r.add(split, ("shared-name", str(replicate), str(epoch)))
out = r.manifest()
seeds = [x["seed"] for x in out["independent_streams"]]
self.assertEqual(len(set(seeds)), 1500)
self.assertEqual(out["training_evaluation_seed_overlap"], 0)
self.assertTrue(all(0 <= s < 2**63 for s in seeds))
def test_alias_explicit_not_independent(self):
r = m.SeedRegistry(461)
seed = r.add("training", ("paired", "0"))
self.assertEqual(seed, r.alias("training", ("frozen", "0"), "training", ("paired", "0"), reason="Same prescheduled CS inputs, different learning policy"))
out = r.manifest()
self.assertEqual(out["independent_count"], 1)
self.assertEqual(out["alias_count"], 1)
self.assertFalse(out["aliases_count_as_independent_replicates"])
self.assertTrue(m.validate_manifest(out))
def test_no_cross_split_alias(self):
r = m.SeedRegistry(1)
r.add("training", ("a",))
with self.assertRaisesRegex(ValueError, "Cross-split"):
r.alias("evaluation", ("a",), "training", ("a",), reason="Not permitted")
def test_no_alias_chain(self):
r = m.SeedRegistry(1)
r.add("training", ("a",))
r.alias("training", ("b",), "training", ("a",), reason="Control")
with self.assertRaisesRegex(ValueError, "not another alias"):
r.alias("training", ("c",), "training", ("b",), reason="Invalid chain")
def test_no_silent_duplicate(self):
r = m.SeedRegistry(1)
r.add("training", ("a",))
with self.assertRaisesRegex(ValueError, "Duplicate"):
r.add("training", ("a",))
with self.assertRaisesRegex(ValueError, "Duplicate"):
r.alias("training", ("a",), "training", ("a",), reason="Invalid")
r.alias("training", ("b",), "training", ("a",), reason="Control")
with self.assertRaisesRegex(ValueError, "Duplicate"):
r.add("training", ("b",))
def test_collision_fails_without_resampling(self):
r = m.SeedRegistry(1)
with patch.object(m, "seed_for", return_value=(123, "a" * 64)):
r.add("training", ("a",))
with self.assertRaisesRegex(ValueError, "Seed collision"):
r.add("evaluation", ("b",))
self.assertEqual(len(r._streams), 1)
def test_mutations_detected(self):
r = m.SeedRegistry(1)
r.add("training", ("a",))
original = r.manifest()
for mutate in (lambda x: x["independent_streams"][0].update(seed=7),
lambda x: x.update(training_evaluation_seed_overlap=1),
lambda x: x.update(independent_count=2),
lambda x: x.update(payload_sha256="a" * 64),
lambda x: x.update(training_executed=True)):
corrupt = copy.deepcopy(original)
mutate(corrupt)
with self.assertRaises(ValueError):
m.validate_manifest(corrupt)
def test_manifest_copy_not_mutable_alias(self):
r = m.SeedRegistry(1)
r.add("training", ("a",))
out = r.manifest()
out["independent_streams"][0]["key"][0] = "changed"
self.assertEqual(r.manifest()["independent_streams"][0]["key"], ["a"])
self.assertTrue(m.validate_manifest(json.loads(json.dumps(r.manifest()))))
def test_json_numeric_boolean_type_changes_rejected_even_when_rehashed(self):
r = m.SeedRegistry(1)
r.add("training", ("a",))
for field, replacement in (("training_executed", 0), ("independent_count", 1.0)):
for rehash in (False, True):
corrupt = r.manifest()
corrupt[field] = replacement
if rehash:
payload = {key: value for key, value in corrupt.items() if key != "payload_sha256"}
corrupt["payload_sha256"] = hashlib.sha256(m._canonical(payload).encode("utf-8")).hexdigest()
with self.subTest(field=field, rehash=rehash), self.assertRaises(ValueError):
m.validate_manifest(corrupt)
def test_reject_invalid_values(self):
for master in (True, -1, 2**63, 1.0, "1"):
with self.assertRaises(ValueError):
m.SeedRegistry(master)
for key in ([], "a", [""], [" a"], ["a "]):
with self.assertRaises(ValueError):
m.seed_for(1, "training", key)
with self.assertRaises(ValueError):
m.seed_for(1, "unknown", ["a"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,171 @@
import copy
from collections import Counter
import unittest
from unittest.mock import patch
import seed_registry
import trial_plan as m
class TrialPlan(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.original = m.build_trial_plan(461)
def test_complete_reconstruction_and_budget(self):
self.assertTrue(m.validate_trial_plan(self.original))
budget = self.original["budget"]
self.assertEqual(budget["training_control_reversal_epochs"], 68000)
self.assertEqual(budget["training_control_reversal_steps"], 99600000)
self.assertEqual(budget["minimum_biological_seconds_excluding_evaluation"], 10760)
self.assertEqual(budget["minimum_steps_excluding_evaluation"], 107600000)
self.assertFalse(budget["evaluation_probe_inventory_complete"])
self.assertIsNone(budget["evaluation_epochs"])
self.assertFalse(self.original["plan"]["training_executed"])
def test_exact_cells_five_training_replicates_not_evaluation_streams(self):
cells = {}
for block in self.original["plan"]["blocks"]:
key = block["mode"], block["dan_type"], block["initial_reinforced_cue"]
cells.setdefault(key, []).append(block["replicate"])
self.assertEqual(len(block["evaluation_streams"]), 2)
self.assertEqual(len(cells), 8)
self.assertTrue(all(replicates == [1, 2, 3, 4, 5] for replicates in cells.values()))
def test_all_epoch_control_roles_and_matched_delayed_slots(self):
for block in self.original["plan"]["blocks"]:
for suite in block["suites"]:
assay = suite["assay"]
arms = {a["arm"]: a for a in suite["arms"]}
self.assertEqual(tuple(arms), m.REVERSAL_ARMS if assay == "reversal" else m.ARMS)
expected_steps, expected_us = (1800, 1500) if assay == "delayed" else (1200, 900)
for arm, spec in arms.items():
epochs = spec["epochs"]
self.assertEqual(len(epochs), 150 if arm == "unpaired" else 100)
cues = Counter(e["cs"]["cue"] for e in epochs if e["cs"] is not None)
self.assertEqual(cues, Counter() if arm == "dan_only" else Counter(A=50, B=50))
us = [e for e in epochs if e["us"]["requested"]]
self.assertEqual(len(us), 0 if arm == "cs_only" else 50)
self.assertEqual(len({e["us"]["inventory_id"] for e in us}), len(us))
for i, e in enumerate(epochs):
self.assertEqual(e["epoch_index"], i)
self.assertEqual(e["steps"], expected_steps)
self.assertEqual(e["us"]["nominal_slot_tick"], expected_us)
self.assertEqual(e["us"]["tick"], expected_us if e["us"]["requested"] else None)
self.assertIsNone(e["us"]["rng_seed"])
self.assertEqual(e["us"]["rng_draws"], 0)
self.assertEqual(e["update_enabled"], arm != "frozen")
self.assertEqual(e["D_command"], 1.0 if e["us"]["requested"] and arm != "d_zero" else 0.0)
if arm == "random_reward":
self.assertEqual(Counter(e["cs"]["cue"] for e in us), Counter(A=25, B=25))
if arm == "unpaired":
self.assertTrue(all(e["cs"] is None for e in us))
if arm == "dan_only":
self.assertEqual(sum(not e["us"]["requested"] for e in epochs), 50)
def test_alias_binding_same_occurrence_even_unpaired_insertion(self):
for block in self.original["plan"]["blocks"]:
for suite in block["suites"]:
base = suite["arms"][0]
lookup = {(e["cs"]["cue"], e["cs"]["occurrence"]): e["cs"]
for e in base["epochs"]}
base_order = [(e["cs"]["cue"], e["cs"]["occurrence"]) for e in base["epochs"]]
for arm in suite["arms"][1:]:
actual_order = []
for e in arm["epochs"]:
if e["cs"] is None:
continue
cs = e["cs"]
source = lookup[(cs["cue"], cs["occurrence"])]
self.assertEqual(cs["seed"], source["seed"])
self.assertEqual(cs["source_stream_id"], source["stream_id"])
self.assertNotEqual(cs["stream_id"], source["stream_id"])
actual_order.append((cs["cue"], cs["occurrence"]))
if arm["arm"] != "dan_only":
self.assertEqual(actual_order, base_order)
def test_reversal_not_extra_acquisition_or_new_compartment(self):
for b in self.original["plan"]["blocks"]:
reversal, continuation = b["suites"][2]["arms"]
original = b["initial_reinforced_cue"]
other = b["initial_neutral_cue"]
for spec, cue in ((reversal, other), (continuation, original)):
self.assertEqual(spec["delta_start"], {"kind": "fork", "source": b["block_id"] + "/normal/paired/terminal-delta"})
self.assertEqual({e["cs"]["cue"] for e in spec["epochs"] if e["us"]["requested"]}, {cue})
def test_split_and_replicate_seeds_are_independent_fullwidth(self):
manifest = self.original["seed_manifest"]
all_seeds = [r["seed"] for r in manifest["independent_streams"]]
self.assertEqual(len(all_seeds), len(set(all_seeds)))
by_block = {}
by_split = {}
for row in manifest["independent_streams"]:
by_block.setdefault(row["key"][0], set()).add(row["seed"])
by_split.setdefault(row["split"], set()).add(row["seed"])
self.assertEqual(len(by_block), 40)
blocks = list(by_block)
for i, block in enumerate(blocks):
for other in blocks[i + 1:]:
self.assertFalse(by_block[block] & by_block[other])
self.assertFalse(by_split["training"] & by_split["evaluation"])
self.assertTrue(any(seed > 2**32 for seed in all_seeds))
for block in self.original["plan"]["blocks"]:
for probe in block["evaluation_streams"]:
self.assertIn(probe["seed"], by_split["evaluation"])
self.assertEqual(probe["seed_bits"], 63)
def test_generic_valid_alias_can_still_be_protocol_illegal(self):
for source, destination in (
(("b-rep1", "normal", "CS", "A", "00", "paired"), ("b-rep2", "normal", "CS", "A", "00", "frozen")),
(("b", "normal", "US", "A", "00", "paired"), ("b", "normal", "CS", "A", "00", "frozen")),
(("b", "normal", "CS", "A", "00", "paired"), ("b", "delayed", "CS", "A", "00", "frozen")),
(("b", "normal", "CS", "A", "00", "paired"), ("b", "normal", "CS", "B", "00", "frozen")),
(("b", "normal", "CS", "A", "00", "paired"), ("b", "normal", "CS", "A", "01", "frozen")),
(("b", "normal", "CS", "A", "00", "paired"), ("b", "normal", "CS", "A", "00", "undeclared-control")),
):
r = seed_registry.SeedRegistry(1)
r.add("training", source)
r.alias("training", destination, "training", source, reason="Synthetic prohibited crossing")
self.assertTrue(seed_registry.validate_manifest(r.manifest()))
with self.assertRaises(ValueError):
m._validate_alias_permissions(r.manifest())
def test_mutations_fail_even_after_forging_payload_sha(self):
def first(p):
return p["plan"]["blocks"][0]["suites"][0]["arms"][0]["epochs"][0]
mutations = (
lambda p: p["plan"]["blocks"].pop(),
lambda p: p["plan"]["blocks"].__setitem__(1, copy.deepcopy(p["plan"]["blocks"][0])),
lambda p: p["plan"]["blocks"][0]["suites"][0]["arms"][0]["epochs"].pop(),
lambda p: first(p)["cs"].update(seed=first(p)["cs"]["seed"] & 0xffffffff),
lambda p: first(p)["cs"].update(seed_bits=32),
lambda p: first(p)["cs"].update(odor="post-outcome-selected odor"),
lambda p: p["plan"].update(execution_authorized=0),
lambda p: p["budget"].update(evaluation_epochs=0),
)
for index, mutate in enumerate(mutations):
payload = copy.deepcopy(self.original)
mutate(payload)
payload["payload_sha256"] = m._sha({k: v for k, v in payload.items() if k != "payload_sha256"})
with self.subTest(mutation=index), self.assertRaises(ValueError):
m.validate_trial_plan(payload)
def test_deterministic_small_shuffle_and_fullwidth_seed(self):
a = m._Shuffle(23).shuffled(range(30))
self.assertEqual(a, m._Shuffle(23).shuffled(range(30)))
self.assertNotEqual(a, m._Shuffle(23 + 2**32).shuffled(range(30)))
self.assertEqual(sorted(a), list(range(30)))
with self.assertRaises(ValueError):
m._Shuffle(True)
def test_integer_rejection_advances_counter_not_seed(self):
shuffle = m._Shuffle(23 + 2**32)
with patch.object(m, "_sha", side_effect=["f" * 64, "0" * 64]) as hashed:
self.assertEqual(shuffle.below(3), 0)
self.assertEqual(shuffle.counter, 2)
self.assertEqual(hashed.call_args_list[0].args[0], [m.SHUFFLE_VERSION, 23 + 2**32, 0])
self.assertEqual(hashed.call_args_list[1].args[0], [m.SHUFFLE_VERSION, 23 + 2**32, 1])
if __name__ == "__main__":
unittest.main()
@@ -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)