Skip to content
chengkaidePublic

About

Depth-resolved LA-ICP-MS zircon U-Pb data reduction: Qtegra cps time series to depth-resolved age profiles, feeding ADEPT for plateau ages and MSWD.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

DRUID

CI Docs License: MIT

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
Loading

第一次接触这套方法? 先看 docs/from-first-principles.html。 它从"放射性为什么能当时钟"一路讲到"MSWD 该怎么读",不需要任何前置知识; 每一步都说明为什么这么做、以及做错了会往哪个方向偏。 文档里的每个数字都能用仓库自带的示例批次复现。


一、安装与运行

三种用法,按熟悉程度选。

1. 网页界面(推荐,不需要命令行)

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

2. 命令行(批处理、脚本化)

:: 在包根目录(含 druid/ 的那一层)执行
python -m druid.cli.reduce_batch ^
    --dir "examples\EX2022A" ^
    --out "结果\EX2022A_U-Pb结果.xlsx" ^
    --plot

examples/EX2022A/ 是仓库自带的示例批次:85 个测点的真实数据, 样品名已匿名成 S01…S48,标样原样保留(91500 / Ple / SRM 612)。 装完环境先跑它一遍,能跑通就说明一切就绪。

3. 作为库调用

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 的输入约定对齐(见下节)
运行参数 本次运行的全部参数与被跳过的文件,便于半年后回溯

先看三个数判断数据能不能用:

  1. 标样QC 里 91500 的偏差 —— 绝对值应小于 1%;
  2. 监控标样 Ple 的偏差 —— 绝对值应小于 3%;
  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。

与 ADEPT 接口有关的一条约定

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 专门把这类点挑出来。

逐点剖面图上还画了一条横贯全段的灰虚线=这个不分域平均值,与各域的红线直接 对照:它落在两条红线之间,说明整段平均既不是核也不是边。

深度剖面域 那一列 MSWD 怎么看

年龄_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 的接口

这是本工具存在的意义之一:把「原始信号」与「坪年龄判定」之间的那段路修通, 让 ADEPT 不必也不该去碰 cps。

为什么不由 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.7645 ns 记在 druid/deadtime_ns.txt, 但没有任何代码会自动读它——要用请显式传 --deadtime-ns 14.7645。 之所以不做成默认值:一旦默认开启,历史上所有未校正的结果就不再可比。

八、变更记录

2.8.0

域级与整段年龄的 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 项。

2.7.0

新增「作废测点」识别;修掉 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 项。

2.6.0 之后(文档与只读工具,不改任何数值输出)

  • 新增页面 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)。

2.6.0

参考值来源做成可选档(默认改为 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 条(增项属兼容扩展,不升 handoff schema); 自检 80 → 95(tests/test_qc.py 32→37,新增 tests/test_reference_presets.py 9 条与落地页数字守护 1 条)。 结果 / 标样QC / 深度剖面域 / 不分域年龄 / 剖面窗口 五张表的 结构与列名均未变。

  • 落地页按"5 秒建立信任"重排(纯文档改动,不碰任何数值输出):标题下新增 「一眼看到什么」区块 —— 一张真实测点的逐深度年龄剖面(由仓库内脚本 tools/gen_docs_preview.py 现算后注入)、 三条核心质控判据的状态条(值与判据原文直接取自 qc.assess_batch())、以及 「标样QC」表的全部行。数字由 tests/check_example_batch.py 抓回核对。 另外:文档卡改成带图标的 pill 并标明"适合谁看";十步流水线上方补一条四块的 概要条(窄屏不必横向滚动);导航在窄屏改为汉堡菜单、滚动时高亮当前节; 页脚补 BibTeX 引用与一键复制;「已知限制」改成 「什么情况下 DRUID 不适用」并加重视觉权重。

2.5.0

文档站重构,并把「文档里的数字」纳入回归守护。不改任何数值输出 (结果 / 标样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)。

2.4.0

新增「不分域年龄」表:把整个剥蚀段当作一个域算出一个年龄,全部样品测点 走同一条处理路径 —— 跨测点横向对比从此有了统一基准。

  • 新 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 逐位一致(端到端回归基线逐项不变)。

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 加权平均年龄」 这条刻意约定。

2.2.3

  • 新增 docs/getting-started.html —— 上手指南。 面向第一次拿到这个工具的人:装环境四条命令(每条都写"怎么算成功")、 数据文件夹怎么摆、结果先看哪三个数、九类常见报错对应哪一步、 以及改代码时必须守住的三条边界。7 张手绘 SVG 示意图,单文件自包含、离线可开。 文档里的每个数字都取自示例批次的端到端回归基线,可自行复现。
  • 一批纯卫生修正,不改任何数值输出(端到端回归的基线值逐项未变): 删掉两个全仓无调用的死函数(io.qtegra.channels_of_interest、 depth.figures.multi_page_pdf)及其导出,连带删掉只被前者使用的 UA_MASSES; reduction.ratios 不再自带一份与 core.constants.MASSES_NEEDED 完全重复的通道元组; 网页界面的 HTTP Server 头不再硬编码 druid-webui/2.0,改读 __version__ (新增回归测试钉住 —— 这类"版本号写两处"已经复发过一次); io.report 的裸 except Exception 收窄为 (AttributeError, TypeError, ValueError); 修正两处会把读者带偏的注释(QC 校正里 1σ/2σ 的缩放、去掉扩展名为何不能用 rstrip)。

2.2.2

  • 新增 docs/from-first-principles.html —— 一份从物理原理讲到结果解读的完整说明(10 张手绘 SVG + 一张真实数据剖面图, 单文件自包含)。写它有两个目的:让第一次接触这套方法的人能在一个地方看懂 每一步为什么必要;以及把每个算法决定的前提与已知局限摊开,便于外部挑错。 文档里的所有数字都能用仓库自带的示例批次复现。纯文档改动,不动任何数值。

2.2.1

  • 修掉一个只在 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 是第一个撞上的。

2.2.0

  • 序列表支持 .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 一致。

2.1.0

  • 深度剖面域 表增加 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。

2.0.0

  • 由原先的单文件脚本重构为分层子包(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 . 根本无法构建——本次补上。

About

Depth-resolved LA-ICP-MS zircon U-Pb data reduction: Qtegra cps time series to depth-resolved age profiles, feeding ADEPT for plateau ages and MSWD.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages