Prepare unregistered conditioning evaluation input and update adapters
This commit is contained in:
+6
-2
@@ -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)
|
||||
Reference in New Issue
Block a user