Depth-Resolved U–Pb Isotope Data reduction —— LA-ICP-MS 锆石 U-Pb 定年数据处理工具
把 Qtegra/iCAP 导出的原始计数率(cps)时间序列,还原成带不确定度的年龄、 标样质控表与逐深度的年龄剖面;其中逐窗口剖面可以直接交给 ADEPT 做坪年龄与 MSWD 判定。
在线文档 → https://chengkaide.github.io/DRUID/ 含《从一块岩石到一个年龄》 (从衰变原理讲到 MSWD,9 章 10 图)与 《两个工具的联系与数据流》。
你想干什么 → 看哪一份
| 你想干什么 | 看哪一份 |
|---|---|
| 第一次拿到这个工具,想尽快跑通自己的数据 | docs/getting-started.html —— 上手指南:装环境四条命令、数据怎么摆、结果先看哪三个数、报错怎么定位(在线版) |
| 搞懂每一步为什么这么做、做错了会往哪偏 | docs/from-first-principles.html |
| 想知道"一段一段地读"比"一个测点一个年龄"多得到什么 | docs/segmented-reading.html |
| 想知道 DRUID 与 ADEPT 之间到底传了什么 | docs/druid-adept-dataflow.html |
| 要改代码 / 长期维护这个仓库 | AGENTS.md |
| 只要一份能在实验室里照着敲的最短说明 | 使用说明.txt |
名字取 "Depth-Resolved" 不是修辞:本工具与只做整段积分的常规流程最大的区别, 是它把每个测点当成一条随时间(即随坑深)变化的信号来处理——分馏按深度校正、 年龄域按深度划分。ADEPT 负责在这条剖面之上挑坪,两者是同一条链的两端。
%%{init: {"flowchart": {"wrappingWidth": 400, "nodeSpacing": 26, "rankSpacing": 34}}}%%
flowchart TD
IN["① 原始计数率 cps<br/>一个测点一个 Qtegra CSV"]
subgraph DRUID["DRUID —— 测出来:每个深度上的年龄是多少"]
direction LR
D2["② 净信号<br/>剥蚀区间识别 · 扣气体空白"]
D3["③ 校正后的比值<br/>干扰校正 · 普通铅<br/>标样夹逼归一化 · F(τ) 深度分馏"]
D4["④ 逐窗口年龄 ± 1σ<br/>另出整段年龄一份"]
D2 --> D3 --> D4
end
XL["结果.xlsx —— 六个工作表<br/>结果 · 标样QC · 深度剖面域<br/>不分域年龄 · 剖面窗口 · 运行参数"]
WIN["《剖面窗口》表<br/>Analysis · Time · Age68 · Age68_1s"]
subgraph ADEPT["ADEPT(R 包)—— 读出来:哪些区间构成真实的坪"]
direction LR
A5["⑤ 分段<br/>在窗口序列上找变点"]
A6["⑥ 过滤<br/>范围 · 方差 · 最短坪长 · 方向"]
A7["⑦ 坪年龄 + MSWD"]
A5 --> A6 --> A7
end
IN --> DRUID
DRUID --> XL
XL -->|"只交出这一张"| WIN
WIN --> ADEPT
XL -.->|"并排写出"| HJ["结果.handoff.json<br/>给程序看的质控结论<br/>判据 · 前提 · 交接风险"]
classDef tool fill:#eaf4ef,stroke:#1d5c45,stroke-width:1.4px,color:#12261f
classDef io fill:#f4f5f7,stroke:#8d9aa1,stroke-width:1.2px,color:#333d42
class D2,D3,D4,A5,A6,A7 tool
class IN,XL,WIN,HJ io
style DRUID fill:#fbfdfc,stroke:#9db8a8,color:#1d5c45
style ADEPT fill:#fbfdfc,stroke:#9db8a8,color:#1d5c45
第一次接触这套方法? 先看 docs/from-first-principles.html。 它从"放射性为什么能当时钟"一路讲到"MSWD 该怎么读",不需要任何前置知识; 每一步都说明为什么这么做、以及做错了会往哪个方向偏。 文档里的每个数字都能用仓库自带的示例批次复现。
三种用法,按熟悉程度选。
Windows 双击 启动数据处理工具.bat,macOS 双击 启动数据处理工具.command。
窗口里会打印一个 http://127.0.0.1:端口 的地址,用浏览器打开它,之后全程在网页上点:
选文件夹 → 看检查报告 → 开始处理 → 下载结果。关闭那个窗口即退出。
两个启动脚本都不自动弹浏览器(地址由你确认一遍,免得
webbrowser挑到意外的浏览器); 想自动打开就把脚本末尾的--no-browser去掉。 拿到dist/DRUID.exe的话注意:那是 Windows 专用的,macOS 上跑不了。 源码本身是纯 Python,Mac 上python3 -m pip install -e .即可(见使用说明.txt)。
:: 在包根目录(含 druid/ 的那一层)执行
python -m druid.cli.reduce_batch ^
--dir "examples\EX2022A" ^
--out "结果\EX2022A_U-Pb结果.xlsx" ^
--plotexamples/EX2022A/ 是仓库自带的示例批次:85 个测点的真实数据,
样品名已匿名成 S01…S48,标样原样保留(91500 / Ple / SRM 612)。
装完环境先跑它一遍,能跑通就说明一切就绪。
from druid import __version__
from druid.workflow import BatchConfig, run_batch
from druid.io.report import export_batch
cfg = BatchConfig(data_dir="examples/EX2022A")
result = run_batch(cfg)
export_batch(cfg, result, version=__version__)
result.results # 逐点结果表
result.qc # 标样 QC 表
result.windows # 逐窗口年龄剖面(供 ADEPT 使用)pip install -e .
druid-reduce --dir 某批次目录 --plot
druid-gui装与不装都能用;不装时在包根目录(含 druid/ 的那一层)跑
python -m druid.cli.xxx 即可。
改过任何东西、或者换了一台机器之后跑一下:
python tests\run_all.py不需要装 pytest。装了开发依赖(pip install -e ".[dev]")的话,
python -m pytest -q 是等价且报告更清楚的一条路——CI 用的是这条。
自检按层铺开(现在 175 项),覆盖的是"改一处不会悄悄改坏另一处":
| 层 | 文件 | 钉的是什么 |
|---|---|---|
| 度量衡 | test_geochronology.py |
年龄 ⇄ 比值往返、r75_from 的交叉恒等式、207Pb/206Pb 的比值下限不是 0 |
| 物理模型 | test_core_models.py |
Stacey–Kramers 单调性与钳位、死时间公式与 2 倍钳位、"τ≤0 时不返回引用" |
| 心脏 | test_reduction.py |
reduce_interval 的每一条契约:缺通道报错、n<5 返回 None、204Pb 不显著就不扣普通铅、ρ 值域、fix 模式、jackknife σ ∝ 1/√n |
| 深度剖面 | test_depth.py |
BIC 分割的四道闸门、边界精修、merge_close 的两条判据、真域/过渡带判定、F(τ) 与 σ 的两路正交合成 |
| 入口分流 | test_io_sequence.py |
样品名规范化先于角色判定、玻璃标样拦截、别名表与自定义标样名的关系 |
| 统计内核 | test_statistics.py |
卡方上尾概率(参考值取自 R)、加权平均、MAD 判离群 |
| 输入边界 | test_input_boundary.py |
Qtegra CSV 解析、剥蚀区间、气体空白、窗口切分(合成数据) |
| 质控/交接 | test_qc.py · test_handoff.py |
检查项等级与用途清单、.handoff.json 契约 |
| 参考值来源 | test_reference_presets.py |
五档预设与默认档唯一出处 |
| 打包 | test_packaging.py |
版本号一致、入口点可调用、网页资源齐全,外加"仓库里不许出现真实地名 / 样品号 / 用户名"的护栏 |
还有一项端到端检查,它跑得慢,所以单独调用:
python tests\check_example_batch.py它把 examples/EX2022A/(85 个测点)真跑一遍,比对标样 QC、样品年龄分布与
深度结构分布。合成数据只能覆盖边界,覆盖不了"数值算得对不对" ——
改了算法就该跑它。CI 每次都跑。
依赖:numpy pandas matplotlib openpyxl xlrd。
不需要 scipy——年龄方程的不动点迭代与二分法都是自己实现的。
网页界面只用标准库(http.server),无额外依赖、无 npm、无构建步骤。
一个批次放在一个文件夹里,文件夹名即批次名:
EX2022A\
EX2022A_LIST.csv ← 测点序列表:第 1 列文件名,第 2 列样品名(无表头)
EX2022A_1.csv ← 每个测点一个 Qtegra 导出的 CSV
EX2022A_2.csv
...
序列表可以用 .xls / .xlsx(仪器工作站直接导出的格式),
也可以自己存成 .csv / .tsv 文本 —— 两列几十行,文本能 diff、
能在 code review 里看懂,examples/EX2022A/ 用的就是 CSV。
要点:
- 序列表必须叫
<目录名>_LIST.<扩展名>(大小写不敏感,_LIST与_list都认); - LIST 里登记的文件名不带
.csv后缀(Qtegra 导出的默认格式); - 样品名决定角色,识别规则见下表;
- 每个 CSV 必须含 202 / 204 / 206 / 207 / 208 / 232 / 238 全部通道。
| 样品名里出现 | 判定为 | 在流程里的作用 |
|---|---|---|
参数 --primary(默认 91500) |
主标 | 算归一化因子 F,决定绝对年龄是否成立 |
参数 --secondary(默认 Ple) |
监控标样 | 估外部重现性 σext、QC、二次校正 |
SRM / NIST / 612 / 610 |
玻璃标样 | 不参与 U-Pb(给它定年没有意义) |
| 其余 | 未知样品 | 真正要定年的对象 |
网页界面会先做一次检查,缺文件、缺序列表当场告诉你,不必等跑完。
纯标样批次(只有 91500 没有未知样品)也能处理:只输出标样 QC, 深度域判别与二次校正自动跳过。
没有主标的批次不会崩,而是退化为「未校准 / 原始比值」诊断模式,
并在结果表里标注 校准状态 = 未校准——这种年龄只能作相对参考,不可用于定年。
十步流水线,每一步都能在结果表的「运行参数」里追溯到所用参数。
| 步 | 做什么 | 关键点 |
|---|---|---|
| ① | 读序列 LIST | 文件名 → 角色 |
| ② | 逐点装载 + 整段还原 | 读 CSV → 死时间 → 找剥蚀段 → 扣空白 → 裁边;单点失败不影响整批 |
| ③ | 主标建立深度剖面 | 供 F(τ) 使用 |
| ④ | 构造夹逼关系 | 每个测点取前后各一个最近的主标 |
| ⑤ | 算两套整段比值 | simple(单一因子)与 ftau(逐窗口 F(τ) 后按计数合成) |
| ⑥ | 主标归一化因子 F | 沿序号线性插值,不是全程常数;MAD 稳健剔除离群主标 |
| ⑦ | 估计外部重现性 | 由监控标样散度扣除内部精度得到 σext |
| ⑧ | 逐点年龄与不确定度 | 三个体系(206/238、207/235、207/206)+ 协和度 |
| ⑨ | 深度剖面年龄域判别 | 滑动窗口 → BIC 分割 → 边界精修 → 合并伪分域 |
| ⑩ | 标样 QC + 二次校正 | 加权平均 + MSWD;用监控标样做基体匹配再校准 |
1. 204 普通铅校正必须做显著性检验。 204Pb 通常只有几~几十 cps,而 204Hg 本底可达数百 cps,两者做差之后残余往往 就是噪声。若 204Pb 不显著却强行扣除,等于把噪声当普通铅扣掉——而这份偏差几乎 全落在 207Pb/206Pb 上:238U/235U ≈ 137.8,放射成因的 207Pb 本来就比 206Pb 少得多(不是“计数少”,见下面第 2 条)。
2. 207Pb/206Pb 不做 F(τ) 逐窗口深度校正。 它是 Pb 同位素比值,几乎不随坑深分馏(随坑深漂移的是 Pb/U 这类元素对比值); 而窗口级 R76 的计数统计已在 1% 量级(4 s 窗口的 207Pb 净计数中位约 9×10³), 能指望的 Pb-Pb 漂移远小于它,套 F(τ) 只会注入噪声。 它仍然参与归一化(F76),只是不做逐深度校正。
3. 剥蚀区间用两级阈值,以峰值比例阈值为主。 只用「本底 + kσ」会把 washout 拖尾一起算进来(关激光后 10 s 仍有 100~200 cps), 把大量零信号混入积分窗口。
结果 Excel 有六个工作表:
| 表名 | 内容 |
|---|---|
结果 |
每行一个测点:U/Th 计数率、三种年龄与不确定度、协和度、深度结构 |
标样QC |
主标与监控标样的加权平均年龄、MSWD、与推荐值的偏差 |
深度剖面域 |
检出多个年龄域的测点,逐个域列出归一化深度 τ、年龄与 MSWD。τ = 0 是该测点的积分段起点、τ = 1 是积分段终点(两端各裁掉 trim 秒、默认 1.5 s,不计入剖面 —— 所以 τ=0 并不是"激光刚打开"那一刻) |
不分域年龄 |
全部样品测点各一行:整段当一个域时的年龄与 MSWD,以及与分域结果、整段积分的差 |
剖面窗口 |
逐窗口年龄剖面,列名按 ADEPT 的输入约定对齐(见下节) |
运行参数 |
本次运行的全部参数与被跳过的文件,便于半年后回溯 |
先看三个数判断数据能不能用:
标样QC里 91500 的偏差 —— 绝对值应小于 1%;- 监控标样 Ple 的偏差 —— 绝对值应小于 3%;
- 协和度落在 90–110% 的样品占比 —— 应大于 85%。
三项都过,数据可用。
上面这些判断现在也是机器可读的。 每次跑完,除了 Excel,与它并排还会写出
结果\EX2022A_U-Pb结果.xlsx 给人看的表
结果\EX2022A_U-Pb结果.handoff.json 给程序看的质控结论与交接契约
JSON 里是 26 条带稳定 key 的检查项(分 fail / warn / info / pass
四级),外加三样以前只写在文档里、下游只能靠猜的东西:
- 数值的定义:
Age68_1s是 1σ、年龄取的是哪一列、剥蚀窗口用的是哪个区间; - 前提:外部重现性 σext 是本批监控标样实测出来的,还是没监控标样时 退回的假设值(0.70%)—— 后者不能用误差棒去论证年龄一致性;
- 交接风险:哪些测点交给 ADEPT 会被静默丢弃(老核 >=1000 Ma、 可用窗口不足 10 个),各多少个、为什么。
还有一块 whole_spot:全部样品测点的整段(不分域)年龄,每个测点一行,
带一个可机读的 mswd_compatible 布尔量 —— 下游不必自己看着 MSWD 猜阈值。
下游按 key 分支,不要按标题文字匹配(文字会改,key 不会)。增删检查项属于
兼容扩展,不升 schema;改 key 名、改语义、改类型才算破坏性改动,那时才升。
无论哪种情况,下游都应当容忍未知的检查项。
命令行的末尾也会把同一份结论打印出来(fail 项在前),所以只看控制台也不会
漏掉「这批数据能不能用」。不想要这个文件就加 --no-json。
handoff.json 故意不给每个样品的加权平均年龄,并在
intentionally_omitted 字段里写明了原因:样品内各测点的散布通常远大于各自的
误差(实测岩样级池化 MSWD 可达 10³~10⁴),加权平均只能靠剔掉大部分测点来达标,
得到的数看着精确、实际是被挑选出来的子集。要出岩样年龄,筛选标准得来自地质
证据(CL 图像、Th/U、协和度),不能来自 MSWD。
深度剖面域 只收多域点 —— 均一点在里面没有行,所以它讲不了"这批数据
整体怎么样"。不分域年龄 补上这一块:每个样品测点都有一行,内容是"把
整个剥蚀段当作一个域、不做任何分域"时算出的那一个年龄。它与 深度剖面域
用同一套窗口、同一个 F(τ)、同一个反比方差加权算法,只差"分域"这一步。
表里并排三种算法,可以直接相减:
| 列 | 是什么 |
|---|---|
年龄_Ma / s2_Ma / MSWD |
不分域:全部窗口反比方差加权平均 |
主域年龄_Ma / 主域n_win |
分域:窗口数最多的那个真年龄域 |
Δ年龄_pct |
不分域 − 主域。这个差就是"分域"这一步对这个测点做了什么 |
整段积分年龄_Ma、Δ整段_pct |
结果表里的 年龄206_238(整段 cps 积分)。它是另一种"不分域"取值,与窗口加权平均不是同一个算法,在多域点上可以差 20% 以上 |
关键在 判定 这一列:
整段常数 整段的散布与各自的不确定度相符 → 这个年龄可以直接用
整段非常数 整段不是一个常数年龄 → 它是几个年龄的加权混合值,或
被残余倾斜拉出来的数。**只可用于横向对比,不可定年。**
这不是理论上的顾虑:仓库自带的示例批次 EX2022A(48 个样品测点)里只有 5 个(10%) 的整段 MSWD 与常数模型相容,中位 MSWD 4.4,最大的到 196。 所以:
- 想知道"这个测点要不要分域" → 看它的
判定、MSWD、Δ年龄_pct; - 想横向比较一批测点(同一套算法) → 用
年龄_Ma,但要在判定=整段常数的子集里比,否则比的是一堆混合值; 判定=整段非常数且域数=1的点要特别当心:分域没能把它拆开,但整段在 统计上已经不是常数了 —— 多半是未完全校正的残余倾斜。质控里的samples.unresolved_structure专门把这类点挑出来。
逐点剖面图上还画了一条横贯全段的灰虚线=这个不分域平均值,与各域的红线直接 对照:它落在两条红线之间,说明整段平均既不是核也不是边。
年龄_Ma 本身就是反比方差加权平均(不是算术平均),MSWD 与
MSWD_概率 都在这个加权平均上算,与 R 端 ADEPT 的 MSWD / MSWD probability
完全一致,两边可以直接对照:
MSWD ≈ 1 该域的散布与各自的不确定度相符 → 一个年龄能解释它
MSWD ≫ 1 要么真的有多个年龄混在里面,要么 σ 被低估
(最常见的原因:只算了计数统计,没算外部重现性)
MSWD ≪ 1 σ 被高估了
MSWD_概率 零假设(该域只有一个年龄)下,MSWD 至少这么大的概率。
小于约 0.05 是常规的"不自洽"警戒线。
注意 n_win 只统计有效窗口(σ 缺失或非正的窗口被排除),所以它可能小于
i1 - i0;MSWD 的自由度是 n_win − 1。
不确定度列里,s68_1sig / s68_2sig 分别是 1σ 与 2σ,别拿错——
下游算 MSWD 必须用 1σ,用 2σ 会把 MSWD 缩小到四分之一。
年龄207_235 的不确定度默认仍是近似传播的(把 206Pb/238U 的相对不确定度原样搬过来),
不是从两个比值的协方差严格算出来的。算法自 1.x 起未变,以保证已发表数字可比。
⚠ 这是个已知的偏小,不是中性选择。207/235 = (206/238)×(207/206) 是两个测出来的 比值的乘积,旧算法只搬了其中一个因子,把 207/206 的整份贡献丢掉了 —— 而本批实测相对 1σ 中位 4.26%(207/206)vs 1.45%(206/238),丢掉的是主项 ⇒ 207/235 的 2σ 中位偏小 2.69 倍(13.21 → 35.55 Ma)。 加
--strict-sigma切到严格传播(把 206/238 ↔ 207/206 的误差相关性算进去; σ 与 ρ 必须同基,见判读细目.md§二.2)。只动 σ,不动年龄——年龄207_235与协和度_pct逐位不变。两个比值之间的误差相关系数本身也已随结果表输出:
rho_68_76(内部/刀切基)。 实测中位 −0.12 —— 负号是构造性的:206Pb 涨则 206/238(分子)涨、207/206(分母)跌。 想画协和(Tera–Wasserburg)误差椭圆要用的是 ρ(206/238, 207/235) ≈ +0.29(合成基), 两者不是一回事,别混用。
想先搞懂原理(从放射性衰变一路讲到 MSWD,不需要任何前置知识): docs/from-first-principles.html
想知道两个工具的分工、数据在它们之间如何变化、边界上到底传了什么: docs/druid-adept-dataflow.html
两份都是单文件自包含(图表都内嵌),直接用浏览器打开,不需要联网。
这是本工具存在的意义之一:把「原始信号」与「坪年龄判定」之间的那段路修通, 让 ADEPT 不必也不该去碰 cps。
从 cps 到一个可靠的比值,中间有七步:死时间校正、204Hg 干扰、普通铅显著性检验、 普通铅校正、标样夹逼、F(τ) 分馏校正、jackknife 不确定度。这些都在 DRUID 里。 而且从 cps 逐点推 σ 在物理上就不成立:单 cycle 的 207Pb 只有几十个计数, 真实噪声以闪烁噪声为主而非泊松噪声;cps 本身还已经除过积分时间、做过死时间校正。
剖面窗口 表的列:
| 列 | 含义 |
|---|---|
Analysis |
序号 样品名,作为分组键 |
Time |
窗口中心时刻 (s) |
Age68 |
窗口 206Pb/238U 年龄 (Ma) |
Age68_1s |
该年龄的 1σ (Ma),已含内部 jackknife + 外部重现性 |
Tau / R68 / s68 / ThU / U_cps / f206_pct / n_cycles |
诊断用 |
adept("EX2022A_U-Pb结果.xlsx",
lower_ablation_time = 0, # DRUID 已切好自己的剥蚀窗口
upper_ablation_time = 1e6, # 上界放到最后一个窗口之后
smooth = "none", # 关键:数据已做 F(τ) 校正,剖面本就是平的
calibration_uncertainty = 0) # 关键:σ 已含外部重现性,再乘 3% 就重复计入ADEPT 会读 剖面窗口 表,忽略同一工作簿里其他不含 Age68 的表;
它用反比方差加权平均给出坪年龄,并报出 MSWD 与「散度与给定误差是否自洽」的概率。
三个参数为什么必须显式给:ADEPT 不去猜数据是否已经预处理过——
一个 _1s 列说明不了剥蚀窗口在哪里结束。猜错了会静默改变进入分段的数据点数。
druid/
core/ 纯计算:衰变常数、标准物质库、年龄方程、普通铅、统计 —— 无 I/O、可单独测试
io/ 读写:Qtegra CSV 解析、序列 LIST、Excel 报表、质控交接 JSON
reduction/ 单点还原:装载流程、剥蚀段识别、同位素比值与 jackknife 不确定度
depth/ 深度剖面:滑窗、F(τ) 分馏校正、BIC 年龄域判别、出图
qc.py 质控判定:把「这批数据能不能用」落成带稳定 key 的检查项 —— 只读不重算
webui/ 网页界面:任务管理 / 业务层 / HTTP 服务 / 静态页面
cli/ 命令行入口:reduce_batch(批处理)、serve_ui(网页界面)
workflow.py 编排层:BatchConfig 集中所有可调参数,run_batch 串起十步
分层原则:core 不 import io,io 不 import reduction,依赖只朝一个方向。
qc 只依赖 core,所以 workflow 可以用它而不会绕回来形成循环。
想改算法去 core 或 reduction;想改流程顺序去 workflow;
想加界面控件改 webui/static/,业务逻辑在 webui/handlers.py;
想改「什么算不合格」改 qc.QCThresholds(那是唯一的阈值出处)。
所有可调参数的默认值集中在 workflow.py 的 BatchConfig,
改默认值只改那一处。命令行参数只是把 argv 翻译成 BatchConfig。
一个测点用一个 Tra dataclass 表示(reduction/trace.py):原始信号、净信号、
时间轴、剥蚀区间坐标、样品名与角色都在里面。用 dataclass 而不是长元组,
是为了加字段时不必去改所有调用点。
core/constants.py 是全部数值常量的唯一出处。同一个衰变常数在两处各写一遍,
迟早会改了一处忘了另一处——而这种错误在同位素年代学里不报错,只是悄悄给出
错误的年龄。
- 域级 / 整段年龄的 1σ 仍然是乐观的:2.8.0 起已按窗口重叠校正(把滑动窗口 当独立观测会让 1σ 偏小约 2 倍),但那一条只解释「重叠」这一项。合成数据里仍有 残余偏乐观(450 Ma 两域剖面 1.14 倍),来自漂移 / 分馏 / 普通铅等效应自身的散布。 ⇒ 拿两个域的误差棒去判断"分不分得开"时,别把它当硬边界。
- 标样与样品计数率相差一个数量级时(本项目:91500 约 1×10⁵ cps,示例样品约 1×10⁶ cps),单一线性归一化因子无法完全消除高计数率下的非线性残差,表现为 监控标样系统性偏老。第 ⑩ 步的二次校正是基体匹配的治标办法; 正式发表前应尽量缩小信号强度差距(换小束斑 / 调低能量),或施加可靠的死时间校正。
- 网页界面没有「停止」按钮,这是刻意的:Python 线程无法安全地强制终止,
做了按钮就会变成「点了没反应」。批处理通常两三分钟,等它跑完即可;
实在要中止就关掉黑窗口(服务端任务会继续跑完并写盘)。
服务只监听本机回环地址,不要改成
0.0.0.0。 --deadtime-ns默认为 0(不校正)。需要时应由标样计数率扫描实验标定, 不要凭经验填。本实验室实测值14.7645ns 记在druid/deadtime_ns.txt, 但没有任何代码会自动读它——要用请显式传--deadtime-ns 14.7645。 之所以不做成默认值:一旦默认开启,历史上所有未校正的结果就不再可比。
域级与整段年龄的 1σ 默认按窗口重叠校正。
- ⚠ 这是本项目第一个会改变历史数值输出的版本:
深度剖面域与整段两张表的s2_Ma会变大(示例批次实测中位 ×1.84 / ×1.96);年龄、MSWD、判定、 域数一律不变。要复现 2.7.0 的数值,加--no-overlap-correct。 - 问题:深度剖面用
win = 4 s / step = 1 s的滑动窗口,相邻窗共用 3/4 的计数。 把它们当独立观测去加权平均,se = 1/√Σw会系统偏小 —— 合成数据实测域级 1σ 偏乐观 2.4~2.9 倍。 - 修法:新增
core.statistics.overlap_factor(n, step/win),取两窗的数据重叠比例 作相关系数(ρ_d = max(0, 1 − d·step/win),纯几何、无待定常数),把 1σ 放大 √K;win = 4 / step = 1时 K → 4,即 ×2。 - 只改 σ,不动判据:MSWD 同样被重叠压小,但那个方向是「漏杀非错杀」的刻意宽松,
校正不碰它。
剖面窗口(逐窗口)、结果(比例法)、标样 QC(跨测点独立) 三张表逐位不变。 - 验收:450 Ma 两域剖面(30 种子)偏乐观 2.16 → 1.14(理想 1.00)。 残余的 1.14 来自重叠以外的来源,别把它当万能缩放。
- 自检 174 → 175 项。
新增「作废测点」识别;修掉 segment 的一处下标约定缺陷。
- ⚠ 不改变任何数值输出:示例批次端到端回归逐项与基线一致。
- 作废测点(新):序列表里实验者显式写的作废标记(
wrong/void/skip/废弃/作废/无效,整名相等、不做子串匹配,免得误伤wrong-1这类真样品名)现在会被识别为第五种角色,结果表类型列写「作废」。 年龄照算、行照留,但所有按「样品」的汇总(协和度、普通铅、多域率、 中位年龄、QC 摘要)自动排除它们;QC 新增检查项data.void_spots(只在真有作废点时才产出,所以常规批次的检查项清单不受影响)。 来历:实测某批 4 个测点的样品名就叫wrong,年龄约 1000 Ma 而该批群体 约 155 Ma —— 明显是打偏或打到了 91500 标样上;这类点当普通样品会静默污染统计。 - 修
segment的下标约定:它内部会滤掉非有限窗口,却把滤后下标返回给 按未过滤剖面索引的调用方 ⇒ 只要剖面里有 NaN,此后所有下标整体左移且 不报错。现改为返回原数组下标(符合它自己的 docstring)。实测全库 26314 个 窗口零 NaN ⇒ 修它在本库上是恒等映射、基线不变;tests/test_depth.py两条守着(契约 + 与refine_domains的端到端一致)。 - 自检 169 → 171 项。
- 新增页面
docs/segmented-reading.html—— 《一段一段地读:分域之后多得到了什么》:用示例批次的 48 个样品测点、 102 个年龄域,量化"分段处理"相对"一个测点一个年龄"的增量与代价。 结论有三条:① 分域这件事本身没有提高分辨力(协和平面并不比年龄轴更有分辨力、 协和度轴几乎判不出,真正更有分辨力的是化学台阶);② 多出来的是域级协和度与协和度沿深度的单调性 这两条坪图没有的轴;③ 把域当独立观测汇总会伪造精度(池化 1σ 缩小约 1.7 倍, 而 χ²/dof 中位 16.6)—— 所以"多加测点、再切细"并不等于更准。 页面上展示的 σ 一律按合成数据实测的乐观倍数放大(域级 1σ 偏窄约 1.9 倍, 见下一条),并同时列出工具自报的读数作对照。 页面上的每个小数与三张图都由tools/gen_docs_segmented.py从模板tools/segmented_reading.page.html现算注入,--check可与已提交页面比对(模板与产物是两个文件:把页面本身当模板会让 生成过一次之后--check永远说"一致",那种检查比没有更糟)。 - 新增只读工具
tools/synth_sigma_audit.py: 量"流水线报出的域级 1σ 到底被低估了多少倍"。真数据没有真值,所以用tools/synth_profile.py造真值已知的两域剖面,跑完run_batch后算pull = (报回年龄 − 真值) / 报出的 1σ。两档对照实测(2026-10-11): 只加计数统计时 pull 标准差 ≈ 1.19(这一层是诚实的);全套效应(+普通铅 5%) 时 pull 标准差 1.81、|pull| ≤ 1 只占 41.7%、median|pull| = 1.28 ⇒ 报出的 1σ 偏窄约 1.9 倍。这个倍数是SIGMA_INFLATION的唯一出处,页面直接 import 它;--check重算比对(±0.35)。它不改任何数值输出,只用于展示。 - 新增页面
docs/extreme-domain-spans.html: 先讲清「协和度到底是什么」——它在年龄–年龄图上是斜率、等协和度是从 原点发散的射线族,在比值空间里则是一族曲线;再逐个复核 12 个跨域跨度500 Ma 的测点,五条判据按硬度排序。图与表由
tools/gen_docs_extreme_spans.py现算后注入。 - 新增只读工具
tools/domain_span_check.py: 计算域级 207Pb/235U 年龄与域级协和度,用来分辨"真继承核"与 "普通铅污染"。 - 线上版不含任何真实样品号 / 批次号 / 地名(
tests/test_packaging.py照旧扫 未跟踪文件);本地实名版只落在汇总库/_分析/(不入 git)。
参考值来源做成可选档(默认改为 Horstwood et al. (2016) 表 S2),并给 「无监控标样」的批次补一条机读的使用范围声明。
⚠ 本版会改变默认数值输出。 这是唯一一处动数字的地方,而且是换一档、 不是纠错 —— 全部年龄按同一个因子整体平移,测点之间的相对关系一点没变 (行数 / 深度结构 / MSWD / 域数一律与 2.5.0 相同)。示例批次:
| 2.5.0 | 2.6.0(默认) | |
|---|---|---|
| 91500 加权平均 | 1059.7543 Ma(−0.2490%) | 1060.2176 Ma(−0.3096%) |
| Ple(监控标样) | 343.2381 Ma(+1.8118%) | 343.3966 Ma(+1.8497%) |
| 样品 206Pb/238U 中位 | 458.0686 Ma | 458.1074 Ma |
| 5–95% | 420.1799 ~ 643.5333 Ma | 420.2161 ~ 643.5834 Ma |
要复现 2.5.0 及更早的数字:加 --ref-preset repo。 constants.STANDARDS
本身一个数都没动,repo 档逐位等于它。
-
新增
constants.REFERENCE_PRESETS:repo/horstwood2016(默认)/wiedenbeck1995/self-consistent/isoclock,每条带 provenance。 默认档由constants.DEFAULT_REF_PRESET一处给出(BatchConfig与core.references都引用它);命令行加--ref-preset,网页界面同一参数。 -
std_ref()/std_age()增加preset=;未知档明确报错,不静默退回默认。 -
qc.py的reference.self_consistency改为按当前档判:默认档 PASS、 切回repo仍如实报 WARN(比值与年龄差 +0.0602%)。 -
qc.py新增uncertainty.claim_scope:把「这批年龄能用来干什么」写成 机读的用途清单。没有监控标样的批次(如样品仓放不下 Ple 时)绝对年龄的 数值照样可用(它是主标归一化出来的,值本身不含 σext), 但误差棒里有一块是写死的假设值 —— 于是absolute_age_value可用、absolute_age_uncertainty不可用,每一项限制都带reasons指回责任 检查项的 key。此前这类批次只得到一句笼统的warn,而"标样偏差略大"也是warn,下游按等级分支根本分不出来。 -
检查项 25 → 26 条(增项属兼容扩展,不升
handoffschema); 自检 80 → 95(tests/test_qc.py32→37,新增tests/test_reference_presets.py9 条与落地页数字守护 1 条)。结果/标样QC/深度剖面域/不分域年龄/剖面窗口五张表的 结构与列名均未变。 -
落地页按"5 秒建立信任"重排(纯文档改动,不碰任何数值输出):标题下新增 「一眼看到什么」区块 —— 一张真实测点的逐深度年龄剖面(由仓库内脚本
tools/gen_docs_preview.py现算后注入)、 三条核心质控判据的状态条(值与判据原文直接取自qc.assess_batch())、以及 「标样QC」表的全部行。数字由tests/check_example_batch.py抓回核对。 另外:文档卡改成带图标的 pill 并标明"适合谁看";十步流水线上方补一条四块的 概要条(窄屏不必横向滚动);导航在窄屏改为汉堡菜单、滚动时高亮当前节; 页脚补 BibTeX 引用与一键复制;「已知限制」改成 「什么情况下 DRUID 不适用」并加重视觉权重。
文档站重构,并把「文档里的数字」纳入回归守护。不改任何数值输出
(结果 / 标样QC / 深度剖面域 / 不分域年龄 / 剖面窗口 五张表
与 2.4.0 逐位一致)。
- 落地页按信息优先级重排:标题下新增一句话价值主张("把 LA-ICP-MS 的 原始信号时间序列,直接转化为带不确定度的逐深度年龄剖面");三条设计原则 改成判断句小标题 + 违反代价 + 展开解释;新增独立一节「先看这三个数」 (91500 偏差 <1% / Ple 偏差 <3% / 协和度占比 >85%,各带实测值与判据原文); ADEPT 接口先给一句话总结再上技术细节。
- 新增「分域前后」三联图 —— 同一个示例批次里的三种剖面形态:
① 均一(域表里根本没有这个测点,整体积分就够)、
② 核边分明(两段平台差 45.6 Ma,一个窗口都没被剥掉)、
③ 复杂变化(三段平台,中间有 8 个窗口不属于任何一段)。
由仓库内脚本
tools/gen_docs_split_figure.py从run_batch()的公开表算出并就地注入docs/index.html;图上每个测点的数字 都由tests/check_example_batch.py按行抓回核对。 图是算出来的,不是手抄的 —— 算法一改、图忘了重新生成,测试会当场红。 - 四份页面补齐 SEO 与社交分享:完整
meta(title/description/keywords/canonical/ 双theme-color)、Open Graph、Twitter 卡; JSON-LD 结构化数据(落地页SoftwareApplication,三份文档页TechArticle+BreadcrumbList);新增docs/robots.txt与docs/sitemap.xml。 - 三份文档页新增深色模式(
prefers-color-scheme;只覆盖:root变量与 三处硬编码色,内联 SVG 一个字节没动 —— 深色下给图一块浅色面板)、 窄屏响应式(≤700px 排版收缩;宽表与宽图加横向滚动容器 + 滑动提示)、 键盘可达(跳过导航链接、:focus-visible焦点环、prefers-reduced-motion关掉入场动画)。 - 落地页改为深色 / 浅色双主题,导航栏一个按钮切换并写
localStorage。 - 对比度实测(浅色主题):正文 14.3:1、次要文字 6.6:1、最淡小字 5.1:1, 全部 ≥ WCAG AA(4.5:1)。
- 修正文档里的两处数字(纯文字,不碰任何输出)。它们属于同一类错误 ——
数字抄下来之后就再没有东西能重算它:
①
depth/windows.py曾经写"win = 4 s 约 25~35 个 cycle per window", 实测是 12~13 个(cycle 周期 0.31871 s;剖面窗口表的n_cycles列一直摆着真值 13)。 ② 有 9 处文件曾经写"窗口级 207Pb 只有约 130 个计数",拿它当 "207Pb/206Pb 不套 F(τ)"的第二条理由 —— 那个数错安了通道:百来个的量级 属于 204Pb(窗口净计数中位约 37),207Pb 实测中位 8,790。结论不变, 理由改为"计数统计已在 1% 量级,能指望的 Pb-Pb 漂移远小于它"。 新增tools/window_scale_facts.py做可重算依据,tests/check_example_batch.py新增「窗口尺度」 一节守着,并把那三个错串全仓禁用(只放行带"曾经写 / 错安了通道" 标记的警示行)。 - 接 ADEPT 的三条补充写进
AGENTS.md§8.1 与docs/druid-adept-dataflow.html: σ 倍数(域表 2σ vs ADEPT 1σ,实测比值 2.0000,读表前先除以 2)、 两家的"有效段"不是同一批窗口(10.7% 的窗口不在任何域里,但仍在剖面窗口表里,所以 ADEPT 还看得见)、ADEPT 自己写出的 xlsx 是坏 XML (中文列名被写成非法控制字节,一律改读它另存的 CSV)。
新增「不分域年龄」表:把整个剥蚀段当作一个域算出一个年龄,全部样品测点 走同一条处理路径 —— 跨测点横向对比从此有了统一基准。
- 新 sheet
不分域年龄:每个样品测点一行(含均一点)。原先只有深度剖面域,而它只收多域点、均一点没有行,于是"分域之后仍报均一、 整段却已不是常数年龄"的测点根本看不见(示例批次里有 10 个这样的点)。 - 逐点剖面图多一条横贯全段的灰虚线=不分域加权平均,与各域红线直接对照。
BatchResult.overall、druid.depth.domains.whole_spot_stats()。- 质控新增
samples.whole_spot、samples.unresolved_structure两条检查项; 交接 JSON 新增顶层块whole_spot(含可机读的mswd_compatible)。 mswd_acceptance()成为 MSWD 相容判据的唯一定义(原先在refine_domains与summarize_segments里各写了一遍)。- 数值中性:
结果/标样QC/深度剖面域/剖面窗口四张表的每一个数 与 2.3.0 逐位一致(端到端回归基线逐项不变)。
- 质控从「控制台里的散文」变成「可机读的检查项 + 交接文件」。
这一版是为了把本工具接进自动化的质控 / 解释流程(以及把剖面喂给 ADEPT 判坪)
做准备。不改任何数值输出(端到端回归基线逐项未变)。
- 新增
druid/qc.py:assess_batch(result)返回 23 条带稳定 key 的检查项, 分fail/warn/info/pass四级。它只读取、不重算 —— 质检层一旦开始算数,就会出现「报告里的数和表里的数不一样」这种最难查的问题。 全部阈值集中在QCThresholds(frozen dataclass),可由BatchConfig.qc_thresholds覆盖。 - 新增
druid/io/handoff.py:与结果表并排写<结果名>.handoff.json(schema = druid.handoff/1)。写盘用「先写.tmp再os.replace」, 因为下游可能在轮询这个文件;allow_nan=False,因为NaN不是合法 JSON。 - 命令行默认写这个文件(
--json PATH换路径、--no-json不写)并把结论 打印在收尾处;网页界面同样写盘,并把它登记成可下载产物。
- 新增
- 把几件长期只存在于文档里的约定变成了带数值的检查项:91500 的参考比值 与参考年龄不自洽(反算差 +0.0602%,会让全体年龄系统性偏老)、 σext 是实测还是假设、二次校正是治标、ADEPT 会丢掉哪些测点。
- 交叉验证:新检查项预测的「ADEPT 会丢哪些测点」与真跑 ADEPT 的记录逐点 一致(6 个真实批次,17/17)。
- 新增
tests/test_qc.py(22 项)与tests/test_handoff.py(15 项), 钉住键集合、等级排序、阈值只改判词不改数值、以及「没有 per-sample 加权平均年龄」 这条刻意约定。
- 新增
docs/getting-started.html—— 上手指南。 面向第一次拿到这个工具的人:装环境四条命令(每条都写"怎么算成功")、 数据文件夹怎么摆、结果先看哪三个数、九类常见报错对应哪一步、 以及改代码时必须守住的三条边界。7 张手绘 SVG 示意图,单文件自包含、离线可开。 文档里的每个数字都取自示例批次的端到端回归基线,可自行复现。 - 一批纯卫生修正,不改任何数值输出(端到端回归的基线值逐项未变):
删掉两个全仓无调用的死函数(
io.qtegra.channels_of_interest、depth.figures.multi_page_pdf)及其导出,连带删掉只被前者使用的UA_MASSES;reduction.ratios不再自带一份与core.constants.MASSES_NEEDED完全重复的通道元组; 网页界面的 HTTPServer头不再硬编码druid-webui/2.0,改读__version__(新增回归测试钉住 —— 这类"版本号写两处"已经复发过一次);io.report的裸except Exception收窄为(AttributeError, TypeError, ValueError); 修正两处会把读者带偏的注释(QC 校正里 1σ/2σ 的缩放、去掉扩展名为何不能用rstrip)。
- 新增
docs/from-first-principles.html—— 一份从物理原理讲到结果解读的完整说明(10 张手绘 SVG + 一张真实数据剖面图, 单文件自包含)。写它有两个目的:让第一次接触这套方法的人能在一个地方看懂 每一步为什么必要;以及把每个算法决定的前提与已知局限摊开,便于外部挑错。 文档里的所有数字都能用仓库自带的示例批次复现。纯文档改动,不动任何数值。
- 修掉一个只在 Windows + 输出重定向时出现的崩溃。 本工具的进度与摘要输出
全是中文;把 stdout 重定向到文件或管道时,Python 用 locale 编码
(英文系统上是 cp1252)编码它,第一句
print("...通过")就抛UnicodeEncodeError: 'charmap' codec can't encode characters, 进程直接死 —— 而且是在跑完批处理、准备打印结果的时候。 现在druid/console.py在 CLI 入口与run_batch()里把输出流切成 UTF-8 (errors="replace"兜底:真编不出来也只丢一个字符,不中断流程)。 交互式控制台一般不踩这个坑(Python 对控制台用宽字符 API), 所以本地开发很难遇到 —— CI 的 windows job 是第一个撞上的。
- 序列表支持
.csv/.tsv文本格式(原来只认.xls/.xlsx), 并优先于二进制格式查找。两列几十行的东西用文本存才能 diff、才能在 review 里看懂; 仓库里的示例批次就是这么存的。.csv/.tsv走显式分隔符,不交给自动嗅探 —— 样品名里有空格(SRM 612)时嗅探可能误判,把样品名劈成两半。 - 随仓库附带示例批次
examples/EX2022A/(85 个测点,样品名匿名成S01…S48,标样原样保留 91500 / Ple / SRM 612)。它有两个用处: 装好环境跑一遍就知道有没有装错;CI 用它做端到端数值回归 (tests/check_example_batch.py),把标样 QC 与年龄分布钉死 —— 测试里的合成数据只能覆盖边界,覆盖不了"数值算得对"。 - 修掉 CLI 收尾摘要的一处统计错误:用
DataFrame.get(col, ...)取列拿到的是 整表那一列,于是 35 个标样测点混进了"样品年龄",中位从 458.1 被抬成 460.9 Ma、5–95% 区间从 420644 撑成 3331044 Ma。而打印出来的n=48让人以为只统计了样品,所以这个错很难被看出来。 license元数据由"内部研究用途"改为 MIT,与仓库根目录的LICENSE一致。
深度剖面域表增加MSWD与MSWD_概率两列。 这两个数一直算得出来 (summarize_segments()内部就在调weighted_mean(),它本来就返回 MSWD), 只是导出时被丢掉了。补上是为了和 R 端 ADEPT 的坪年龄算法直接并列对照—— 两边现在都是"反比方差加权平均 + 卡方上尾概率"。只增加列,既有列与数值未动。- 新增
tests/自检(不依赖 pytest)与 CI(ubuntu × py3.9/3.12/3.13 + windows × py3.13,含 wheel 完整性检查与示例批次端到端回归)。 docs/druid-adept-dataflow.html与AGENTS.md。
- 由原先的单文件脚本重构为分层子包(
core/io/reduction/depth/webui/cli+workflow),并改名upb→druid。 - 修掉三个潜伏缺陷:
workflow.py用了STANDARDS却没导入(传未知主标时给出的 是NameError而不是可用标样清单)、L238硬编码(与core/constants.py各写一遍)、 网页 banner 硬编码版本号v2.0。 pyproject.toml声明了readme = "README.md"但文件不存在,pip install -e .根本无法构建——本次补上。