从一块岩石到一个年龄

LA-ICP-MS 锆石 U-Pb 定年:物理原理、每一步算法、以及结果该怎么读

面向第一次接触这套流程的人 · 对应 DRUID 2.8.0 与 ADEPT 1.3.0 · 所有公式与数值均取自源码

这份文档想解决一个问题。这两个工具(DRUID 和 ADEPT)加起来有七千多行代码,每一步都做了某个具体的决定:为什么要扣这个、为什么要切那个窗口、为什么这个数要除以 n−1。代码里写清楚了,但代码不是给新人读的。于是跑出结果和明白结果如何来的之间仍有一定的间隔。

但其实不复杂,就是一个公式(第 4 章那个),其余全是测定的干扰项。这份文档按这个顺序讲:先讲时钟本身,再讲信号怎么变成数字,再讲每一处校正为什么必要,最后讲结果的每一个数该怎么读、以及读到什么程度就该怀疑它。

为什么值得公开写这些。一套分析流程最危险的时刻,是它开始产出数字、而没有人再检查中间步骤的时候。把每一步摊开写出来,等于邀请别人来挑错——包括我们自己。这份文档里所有关键数字都标了出处(源码文件名或实测批次),你可以逐条核对。

目录

  1. 时间是怎么被"读"出来的
  2. 数据是怎么产生的:一台质谱仪里的七十五秒
  3. 从 cps 到比值:八个必须做的校正
  4. 从比值到年龄:一个公式,和三套时钟
  5. 从剖面到坪:在一条抖动的线上找台阶
  6. MSWD:判断"这个坪自洽吗"
  7. 结果表里每个数是什么意思
  8. 我们怎么知道这些数字是对的
  9. 如果你要参与

一、时间是怎么被"读"出来的

1.1 一块自己会走快的表

想象一个沙漏,但它的沙子不是匀速漏的:剩下的沙子越多,漏得越快。每一粒沙子的漏出概率是固定的,所以一开始沙子多、漏得猛,越到后面越慢,永远漏不完,只是越来越接近空。

这正是放射性衰变的行为。一个238U 原子在任何时刻衰变的概率是固定的,不受它已经等了多久、周围有什么影响——这叫恒定衰变律,是这套定年方法的全部物理基础。写成数学,母体原子数随时间的变化是:

N(t) = N₀ · e−λt   母体还剩多少
D*(t) = N₀ · (1 − e−λt) 子体积累了多少

其中 λ 叫衰变常数,单位是"每年"的倒数。它不是理论算出来的,是实验室反复测出来的物理常数。238U 的 λ 是 1.55125×10⁻¹⁰ /年(Jaffey 等,1971)。这个数字小得出奇——意思是:一个 238U 原子平均要等 64 亿年才衰变一次。

把一个 λ 换算成更直观的"半衰期"(母体剩一半需要的时间,T½ = ln2/λ):

同位素衰变成半衰期在地质时间尺度上意味着
238U206Pb44.68 亿年地球年龄量级的尺子,适合几亿到几十亿年
235U207Pb7.04 亿年快 6.3 倍,老样品里早就积满了
0 0.5 1.0 占初始原子数的比例 12 345 经过的时间(以半衰期为单位) 1 个半衰期 正好剩一半 母体(剩下的 U) 越来越慢地减少 子体(积累的 Pb*) 3–4 个半衰期后趋平
图 1 衰变的基本形状。横轴以"半衰期"为单位——这样两条曲线对任何同位素都长一样:238U 的一个半衰期是 44.68 亿年,235U 的是 7.04 亿年。注意子体(红线)在 3–4 个半衰期后趋于平缓:越老的地质样品,同样的时间增量带来的比值增量越小。这既是"老样品测得更准"的原因(比值大、相对误差小),也是后面所有统计讨论的起点。DRUID 的 core/geochronology.py 用 expm1(λt) 而不是 exp(λt)−1,正是因为年龄很小时这两者相减会丢有效数字。

1.2 为什么必须用"比值"

上面第二个公式看起来可以直接用:量出样品里积累了多少 206Pb,除以母体 238U,再反解 t 就行了。但有一个问题——你永远无法知道"初始有多少 238U"。四十四亿年前这个晶体开始生长时有多少铀,已经无从考证了。

所以我们换一个角度。238U 少了多少,就等于 206Pb 多了多少(假设没有铅跑出去)。设当前铀的原子数是 N:

206Pb*/238U = (N₀ − N)/N = (N₀/N) − 1 = eλt − 1

N₀ 被消掉了,只剩下当前测得到的两个量之比。这就是为什么 U-Pb 定年测的是比值而不是含量。

这个技巧还有一个额外的好处:质谱仪对"某个元素测得准不准"依赖很多不稳定因素(灵敏度漂移、基体效应)。但如果你测的是同一个元素的两个同位素之比(比如 207Pb/206Pb),那些不稳定的因素大多会同时乘在分子和分母上,相除就抵消了。同位素比值比绝对含量稳健得多——这是贯穿整套流程的一个主题,第 3 章还会反复遇到。

1.3 为什么锆石是最好用的那块表

理论上任何含铀矿物都能当时钟。实践中锆石(ZrSiO₄)几乎是唯一的选择,原因有三个,每一个都对应一个具体的麻烦:

最后一条还有另一个侧面,它既是优点也是麻烦:因为结实,一个锆石晶体可以记录多次事件。如果岩浆在几百万年甚至几亿年后又重新活动,新的锆石会在旧晶体的外面继续生长一层。于是你得到一颗有"核"和"边"的晶体,它们的年龄不同。

1.4 一个晶体里的两个年龄

一颗锆石的截面 外层「边」 240 Ma 后来又一次岩浆活动长出来的 内层「核」 400 Ma 更早那次岩浆活动的残余 激光剥蚀坑 ⌀30 μm,深约 25 μm 从表面往下打,穿过边、进入核 坑底 测量得到 年龄—深度 这个坑里测到的年龄 01025 剥蚀深度 (μm) 240330400 年龄 (Ma) 240 Ma 上表面到约 7 μm 400 Ma 10 μm 以下 过渡带 整段积分 = 330 Ma 两个年龄的混合,谁也不是
图 2 问题的来源。左侧是一颗锆石的截面:核形成于 400 Ma,之后又一次岩浆活动在它外面长了 7 μm 厚的一层"边"(240 Ma)。激光坑直径 30 μm、深 25 μm,打下去的时候既穿过边、又进入核,中间还经过一条几微米宽的过渡带。右侧是这个坑里的年龄随深度的分布——如果对整段做一次积分,得到 330 Ma,这个数字不对应任何一次地质事件。随深度分辨的全部意义,就是把 240 和 400 这两个数字重新分开。

传统做法是"躲":在显微镜下挑出均一的区域,把激光对准那里。但一个 20 μm 的坑对一个只有几微米宽的过渡带来说太粗了,你没法用眼睛保证躲开。更常见的结局是——你以为打的是边,其实打进去了核,两条年龄混在一起,最后得到一个"协和度很差"的数字(第 4.4 节讲这个指标),然后整批数据被丢掉。

另一种做法是"用"。既然剥蚀过程中连续测了三十秒、每秒十次,为什么不看它随深度的变化?如果剖面在一段时间里稳定在 240 Ma,然后干净地跳到 400 Ma,两个年龄就都在数据里,只是需要把它们分别提取出来。这就是 DRUID(提取逐深度窗口的年龄剖面)和 ADEPT(在剖面上自动找出这些"稳定的段")的分工。

这套思路成立的前提是:信号真的能分辨出深度的变化。下一章讲这个前提在物理上有多牢靠。

二、数据是怎么产生的:一台质谱仪里的七十五秒

2.1 从激光到计数

要读时钟,得先把晶体的一小块"搬进"仪器。这一步由激光完成,之后的每一步都是在把这团物质转换成电信号:

  1. 激光剥蚀。一束聚焦的紫外激光打在锆石表面,瞬间把几十微米范围内的物质加热到汽化。被剥蚀下来的物质不是气体,而是极细的气溶胶——大量亚微米到微米级的颗粒。
  2. 载气搬运。氦气(有时是氦氩混合)从样品池里把气溶胶吹出来,沿着管路送进质谱仪。这段管路有几十厘米到一米长,气溶胶在里面的传输效率本身就是一个会漂移的量。
  3. 等离子体电离。气溶胶进入氩等离子体炬——温度大约 8000 K,比太阳表面还热。颗粒在这里被完全分解、原子化,然后被电离成带电离子。这一步的效率对不同的元素是不同的,叫"元素分馏"。
  4. 质谱分离与计数。离子被电场加速、通过质量分析器(按质量/电荷比分离),最后打到检测器上。检测器每收到一个离子就输出一个电脉冲,电子学部分数这些脉冲。

输出就是每个质量数、每个时间切片上收到了多少个脉冲。仪器通常把它换算成 cps(counts per second)——每秒计数。

2.2 一次测点的时间序列

① 气体空白(激光关) ② 剥蚀中(激光开,约 30 s) ③ 冲洗拖尾 10¹10²10³ 10⁴10⁵10⁶ 计数率 cps(对数刻度) 阈值 2:本底 + 3% 峰高 —— 主判据,用来砍掉拖尾 阈值 1:本底 + 8σ —— 兜底,防止阈值落进噪声 238U 剥蚀段约 2×10⁵ cps —— 用它来定位剥蚀区间 207Pb 剥蚀段只有约 250 cps,单点计数太少,必须靠窗口平均 01020 304050 607080 90 激光开始后的时间(秒) 采样频率约 10 Hz —— 剥蚀的 30 秒里大约有 300 个数据点,每个点 7 个通道
图 3 一次激光测点的信号形状(纵轴对数刻度——空白段与剥蚀段相差三个数量级)。整套流程建立在一个简单事实上:激光关着的时候测到的只是本底,激光打开的那三十秒才是样品。所以第一件事是找到开关时刻(3.2),第二件事是取"关着的时候"作为气体空白扣除(3.3)。两条水平线是自动定位用的两级阈值:绿色的按"峰高的 3%"截断,负责砍掉冲洗拖尾;褐色的按"本底 + 8σ"兜底。注意红线(207Pb):即使剥蚀期间也只有约 250 cps,而每个采样点只有 0.1 秒——单点不到 25 个计数。这就是为什么必须切成 4 秒窗口来凑计数,也是为什么噪声不能简单套泊松公式。

2.3 cps 不是"计数"——这件事很要紧

看起来把一个时间段的 cps 加起来就得到"总计数",噪声就是 √N。但这个想法有两个漏洞,两个都会在后面的不确定度里出现:

漏洞一:cps 已经除过积分时间,且做了死时间校正

检测器在高计数率下会"数漏"——两个离子在极短时间内先后到达,电子学会把它们算成一个。这叫死时间效应,计数率越高漏得越多,是典型的系统性偏差。校正公式(非延长型)是:

真实计数率 = 观测计数率 / (1 − 观测计数率 × τ)  τ = 死时间,通常几 ns 到几十 ns

本实验室实测值是 14.7645 ns(记在 druid/deadtime_ns.txt),但这个数字默认不启用——因为一旦默认打开,历史上所有未校正的结果就不可比了。要用得显式传参。

漏洞二:真实噪声不是泊松的

泊松统计假设"离子到达是完全独立的随机事件"。但 ICP-MS 上还有一种占主导的噪声源:等离子体闪烁噪声(flicker noise,也叫 1/f 噪声)。它来自等离子体本身的温度与密度起伏,其大小与信号强度成正比,而不是与 √信号成正比。

后果很实际:把积分时间加长,泊松噪声按 √N 下降,但闪烁噪声不怎么下降。所以"多点几个点就更准"在某个点之后就没有收益了。DRUID 的做法是干脆不用解析公式,改用刀切法(jackknife)从数据本身估计离散度——它对两种噪声一视同仁(reduction/ratios.py,第 3.8 节细讲)。

2.4 八个通道分别在测什么

一次测点通常同时采集下面这些质量数。它们的角色很不一样,值得记住:

质量数主要是什么在这套流程里的角色
202202Hg唯一的用途是推算 204Hg 的干扰量。本身不参与年龄计算
204204Pb + 204Hg估算"普通铅"(非放射成因的铅)。信号极弱,是整条链路里最难的一步
206206Pb238U 的衰变产物,信号最强的铅同位素
207207Pb235U 的衰变产物。丰度低得多,信号弱
208208Pb232Th 的衰变产物。本流程里只用于普通铅校正
232232Th算 Th/U 比,用来判断锆石的成因类型(岩性指标)
238238U母体。同时是信号最强、形状最清楚的通道,用来定位剥蚀区间

为什么用 238U 而不是更直接的东西来定位剥蚀?因为定位只需要"信号什么时候变强了",而 238U 的剥蚀段信号通常是 10⁵–10⁶ cps——比任何其他通道都高两三个数量级,形状最干净。reduction/ablation.py 的注释里写着理由:"用 U 是因为它信号最强、形态最清楚"。

2.5 从"一串数字"到"一个年龄",中间隔着什么

到这里我们有了:一次测点、七个通道、几百个时间点、每个点一个 cps 值。目标是:这一点的年龄,以及它的不确定度。

距离还很远。接下来要处理的,是下面这些每一件都会单独污染结果的东西:

问题如果不处理会怎样在哪一步处理
探测器的死时间高计数率时信号被系统性压低,且 Pb/U 压低的程度不同可选校正(3.1)
不确定激光从哪一秒开始把空白段算进样品,比值被稀释两级阈值定位(3.2)
载气里的汞和仪器本底所有通道都被抬高一个常数,比值失真气体空白扣除(3.3)
204Hg 干扰 204Pb把干扰当成普通铅扣掉,207Pb/206Pb 系统性偏低Hg 校正 + 显著性检验(3.4)
晶体生长时自带的铅时钟没有从零开始,年龄偏老普通铅校正(3.5)
剥蚀坑越打越深,Pb/U 逐渐漂移剖面倾斜,可能被误判成"两期年龄"标样夹逼 + F(τ)(3.7)
单点只测几十个计数的统计涨落逐点年龄抖得没法看窗口化 + jackknife(3.6、3.8)

下一章按这个顺序逐个讲。每一步都会给出为什么(物理原因)和怎么做(实际公式),并且在必要时说明"这一步做错了会导致什么方向的偏差"。

三、从 cps 到比值:八个必须做的校正

下面八步按实际执行顺序排列。每一步都回答同一个问题:有没有什么东西正在污染这个比值?有些步骤是必须的,有些是可选的(比如死时间),但即使在"可选"的地方,也说明了为什么默认不启用——因为把一个校正关掉比错误地打开它更安全。

3.1 死时间校正(默认关闭)

检测器在高计数率下会漏计数,前面 2.3 节给了公式。这里补充三件事:

python -m druid.cli.reduce_batch --dir 某批次 --deadtime-ns 14.7645

一个设计上的取舍值得说明。这个参数理论上可以让所有结果更准,但软件选择了"不自动应用一个已知更准的校正"。原因是不确定性转移:打开它,你不知道今天的结果和去年发表的结果能不能直接比较;关着它,至少前后是一致的。这类"故意不优化"的决定在科研软件里很常见,也值得在读代码时留意。

3.2 找到剥蚀区间:为什么要两级阈值

目标很简单:找出激光真正打开的那一段。做法是拿 238U 的信号(最强、形状最干净)去比对阈值。麻烦在于阈值的选取:

两个细节值得注意。第一,"峰值"用的是 98 分位数而不是最大值——最大值可能只是一次随机尖刺(比如一个包裹体被打爆),用它定阈值会让门槛虚高。第二,本底的 σ 用 MAD 估计而不是标准差(σ ≈ 1.4826 × MAD)。原因是本底段里可能混有零星尖峰,标准差会被它们放大,中位数绝对偏差不会。

信号在阈值附近的抖动会产生若干段短暂的"超阈值"区间,所以最后取最长的一段连续区间作为剥蚀段,而不是简单地取第一个超阈值点。

3.3 气体空白:为什么要取"紧挨着剥蚀之前"的那一段

激光关着时测到的信号,来自载气里的杂质(主要是氩气瓶和管路里的汞)、样品池的残留、以及仪器本身的电子本底。这部分必须在所有通道上逐通道扣除。

取哪一段?直觉上取文件开头最方便,但这里取的是剥蚀开始前 15 秒(并且留 0.5 秒的安全间隔,躲开激光实际起跳与电信号之间的延迟)。原因是:气体空白会缓慢漂移——氩气瓶里的汞含量、管路温度、进样系统的状态都在变。时间上离剥蚀最近的那一段,才代表剥蚀那几十秒里的真实本底水平。

扣除之后,净信号允许是负数。这不是错误,是统计学上正常的:信号恰好等于本底时,扣完就在零附近随机波动。这里绝对不能 clip 到零——那会把一半的随机涨落抹掉,引入一个系统性正偏差。ablation.py 的注释专门强调了这一点。

3.4 汞干扰与"这个铅是真的吗"——整条链路最关键的一步

这是整份文档里技术含量最高、也最容易做错的地方。它值得单独一节。

204Pb 是唯一能直接指示"普通铅"的同位素,而普通铅必须扣掉(下一节讲为什么)。但 204 这个质量数上坐着两位乘客:

测到的 204 信号 = 204Pb(想要的) + 204Hg(干扰)
而 204Hg 可以间接算出来:204Hg = 202Hg / 4.35
→ 204Pb = 204测到 − 202测到 / 4.35

Hg 的 202/204 丰度比 4.35 是自然界常数,而 202 这个质量数上没有别的干扰,可以直接读。所以公式本身没问题。问题出在数量级上:

204 通道的残酷现实。真实数据里,204Pb 的信号通常只有几个到几十个 cps;而管路里的 204Hg 本底常常达到数百 cps。我们要的是这两个数的差值。

这是典型的"两个大数相减求小数":204Pb 的残余不确定度会被放大到和它自己同一个量级。实测本批次 91500 主标:扣完汞之后的 i204 在 −6 到 +17 cps 之间纯随机波动——也就是说,一半的测点上它甚至是负的。而负的普通铅在物理上不存在。

于是有了这套流程里我认为最重要的一个决定:先问这个 204Pb 是不是真的存在,再决定要不要扣。

同样按 2σ 门槛判断,三个测点的结局完全不同 零 (=完全没有普通铅) 2σ 门槛 −20−10 +20+30 扣汞之后得到的 i204(cps) ① 越过门槛 确有普通铅 → 执行扣除 ② 均值虽正,误差棒跨过零 它完全可能只是噪声 → 不扣 ③ i204 是负的 负的普通铅不存在 → 不扣 实测 91500 主标:i204 在 −6 到 +17 cps 之间纯随机波动,多数测点落在 ② 和 ③ 这两种情形里
图 4 显著性检验的逻辑。做法是用刀切法估计 i204 这个量自身的标准误 σ(i204),只有满足 i204 > 2σ(i204) 才认为"确实检测到了普通铅"。中间那种情形占了相当大比例,而它正是这一步存在的理由——如果只看"均值是不是正的"就扣,一半的测点会拿噪声去扣真实的铅。

如果不管显著不显著,一律扣除,会发生什么?

后果是方向明确的,而且很严重。假设某个测点其实没有普通铅,但你按噪声估计出的一个正的 i204 去扣:

被扣掉的量相对影响结果
206Pb扣除 i204 × (206/204)普通铅小(206Pb 本身很大)206/238 年龄略受影响
207Pb扣除 i204 × (207/204)普通铅大得多(207Pb 本来就少)207/206 比值系统性偏低

两项相除得到 207Pb/206Pb。207Pb 被多扣了,所以这个比值偏小,而 207Pb/206Pb 年龄随之大幅偏年轻。你会得到一批"看起来很精确、但系统性错了"的年龄,而且因为 207Pb/206Pb 在协和度检验里占重要位置(4.4 节),这种偏差还能伪装成"数据不一致",让你去怪样品。

这条经验值得单独记下来:当一个校正依赖"两个大数相减",而且你对这个差值的显著性没有把握时,默认不校正通常比默认校正更安全。因为"不校正"引入的偏差是随机方向的(有时偏大有时偏小,在大量测点上互相抵消),而"错误校正"引入的偏差是系统性的(一直往一个方向偏)。

3.5 普通铅校正:一个鸡生蛋问题的迭代解法

为什么要扣普通铅:晶体生长时可能裹进了一点点周围环境的铅(来自地壳或地幔的铅,不是铀衰变出来的)。这部分铅的存在会让"钟的读数是零"这个前提不成立,测出的年龄偏老。

要扣掉它,需要知道普通铅的同位素组成——即 206Pb/204Pb、207Pb/204Pb、208Pb/204Pb 三个比值。而普通铅的组成取决于它形成时的年龄:地球的铅同位素组成随时间演化(238U 和 235U 一直在衰变,所以古老的地球铅里 206Pb/204Pb 更低)。这个演化模型叫 Stacey-Kramers 两阶段模型,可以查表得到任意年龄对应的铅组成。

于是问题来了:要算年龄,需要知道铅组成;而铅组成需要知道年龄。

假设年龄 t → 查 Stacey-Kramers 得到铅组成 sk → 扣除后算 207Pb/206Pb
→ 反解新年龄 t′ → 若 |t′ − t| < 0.5 Ma 认为收敛,否则用 t′ 重来

这是标准的不动点迭代。初值取 500 Ma(随便一个中间年龄即可),实测 3–4 轮必然收敛,所以代码里迭代上限直接写死为 4 次(reduction/ratios.py)。如果某轮迭代出现了非物理的结果(比值非正),就放弃校正、走"不扣"的路线——又一次"宁可少扣不可错扣"。

3.6 滑动窗口:为什么窗口要重叠

到这里我们得到的是"整个剥蚀段一个年龄"。但要提取深度剖面,需要把它切成一串更细的窗口。

(上)窗口重叠:宽 4 s,步长 1 s 窗口 1窗口 2 窗口 3窗口 4 窗口 5 这一秒被 4 个窗口共用 012 345 678 激光开始后的时间(秒) (下)归一化到 τ:不同时长的测点叠在同一张图上 测点 A(剥蚀 28 秒,约 28 个窗口) 测点 B(剥蚀 35 秒,约 35 个窗口) τ = 0 τ = 1 归一化深度 τ(坑口 → 坑底)
图 5 窗口切分的两个设计选择。(上)窗口重叠:宽 4 秒、步长 1 秒,相邻窗口共享 3 秒数据。代价是相邻点不再独立(第六章的 MSWD 判据必须留意这一点);换来的是把时间分辨率从 4 秒提到 1 秒——仍然需要 4 秒的数据来压低 207Pb 噪声,但能看到 1 秒尺度的变化。(下)τ 归一化:测点之间的剥蚀时长并不相同(实际数据里从 28 秒到 35 秒都有)。如果横轴用"激光开始后第几秒",两个测点的剖面就没法叠在一起看。归一化到 0–1 之后,τ = 0 恒是这个测点的积分段起点、τ = 1 恒是积分段终点(两端各裁掉 trim 秒、默认 1.5 秒,不计入剖面 —— 所以 τ=0 并不是"激光刚打开"那一刻),所有测点的剖面可以画在同一张图上比较。

窗口参数不是随便定的。太小则单个窗口内只剩几个 cycle(4 s 才 13 个),各通道的计数统计都变差,先撑不住的是计数本来就少的 204Pb;太大则把核-边的过渡带糊在一起,看不出结构。本批实测的合理值是 win = 4 s / step = 1 s——每个窗口里 12~13 个采样周期(cycle 周期实测 0.31871 s)。

窗口里的 204 怎么处理,是个容易被忽略的细节。窗口只有 4 秒,204Pb 的绝对计数可能只有个位数——在这个尺度上独立做 3.4 节的显著性检验就是纯噪声。但普通铅在同一个剥蚀点内是近似恒定的(它来自同一批包裹体)。所以窗口化的时候用的是"fix 模式":把整段算出来的普通铅比例借用到每个窗口(按窗口自己的 206Pb 强度按比例分配)。这样窗口之间的相对变化才不会被 204 的噪声淹没。这个细节如果不做,剖面上会出现大量随机的锯齿,看起来像"多期年龄",其实是噪声。

3.7 剥蚀坑越打越深,比值会自己漂移

这是深度剖面方法必须解决的问题,否则整件事都不成立。

本批实测:206Pb/238U 从坑口到坑底漂移约 20%。这不是仪器故障,是激光剥蚀的固有现象:坑越深,剥蚀出的气溶胶颗粒越粗,粗颗粒在管路里的传输效率低于细颗粒,而 Pb 和 U 在颗粒中的分布又不完全一样。结果就是 Pb/U 比值随深度逐渐降低。20% 是一个巨大的系统误差——如果不管它,一个本该水平的年龄剖面会倾斜 20%,看起来非常像"这个锆石里有两个不同年龄的域",而实际上只有一个。

解决办法是用标样。标样的年龄是已知的("推荐值"),所以在同一台仪器、同一次运行里,标样测出来的比值偏离推荐值多少,就反映了当下的分馏程度。关键在于要在同一个深度上比——因为分馏是深度的函数,不是常数。

F(τ) = R推荐值 / R标样实测(τ)  标样在深度 τ 处偏了多少,认为样品在同一深度也偏了多少

校正后的样品比值 = R样品实测(τ) × F(τ)
(上)标样夹逼:每个样品用时间上前后最近的两个标样 91500 标样 样品 S1 样品 S2 样品 S3 91500 标样 这三个样品共用同一套 F(τ) —— 同时一阶抵消仪器随时间的漂移 标样年龄已知, 偏多少就是当时的 分馏程度 校正前 归一化深度 τ 年龄 (Ma) 480400 看起来像"两个年龄域" 实际上只是分馏造成的倾斜 × F(τ) 校正后 归一化深度 τ 480400 水平了 —— 现在"哪里有坪"才有意义
图 6 分馏校正的两件事。上:标样夹逼(bracketing)——标样(这里是 91500,推荐年龄 1062.4 Ma)穿插在样品之间,用它们的实测比值在每个深度上取平均来构造 F(τ)。用"前后各一个"而不是"整批一个平均",是为了同时一阶抵消仪器随时间的漂移(检测器老化、锥口堵塞)。下:校正前后的对照——不校正的剖面是倾斜的,坡度就是分馏幅度;校正之后应该水平。如果校正模型用错了,倾斜会伪装成假的"多期年龄",而这是最容易让人误判的一类错误。

为什么 207Pb/206Pb 不套这个校正?两个原因。第一,物理上它不该漂移:207Pb 和 206Pb 是同一个元素的两种同位素,它们在剥蚀、传输、电离过程中的行为几乎完全一样,所以它们的比值几乎不受分馏影响——这正是 207Pb/206Pb 年龄对老样品特别有价值的原因之一。第二,实践上不能套:4 秒窗口里的 207Pb 净计数中位约 9×10³,R76 的计数统计已经在 1% 量级,而能指望的 Pb-Pb 漂移远小于它;再乘一个由标样估计出来的 F(τ),等于把标样的噪声注入到样品剖面里,让剖面抖得没法看。所以代码里给 207Pb/206Pb 保留了这个参数位,但实际传进去的是"全 1"(即不校正)。

3.8 不确定度:为什么不用 √N,为什么要两个来源相加

一个年龄数字如果不带不确定度,是没有意义的——因为"453.1 Ma"和"453.1 ± 8 Ma"是两种完全不同的结论。DRUID 给出的逐点不确定度由两部分正交相加:

单点年龄的 1σ 由两个独立来源合成 ① 内部误差 σ68 · F 同一个窗口内部,各次采样之间的涨落 来源:计数统计 + 等离子体闪烁噪声 ② 外部重现性 σ_ext · R68 不同测点之间的差异:仪器漂移、基体差异、剥蚀条件 来源:由监控标样的实测散度反推 + σ_点 = hypot( σ68 · F , σ_ext · R68 ) 两个都是绝对的比值误差,所以平方相加再开方(正交合成) 再换算成年龄误差:σ_age = σ_R / ( λ · (1 + R) ) ÷ 10⁶ (因为 age = ln(1+R)/λ)
图 7 不确定度的合成方式。只算内部误差会系统性偏小——它衡量的是"同一窗口内各次采样的一致程度",抓不到"这个测点和下一个测点之间仪器状态变了多少"。外部重现性由监控标样反推:把观测到的总散度做方差分解 σ²外部 = σ²观测 − σ²内部,再夹在 0.3%–5% 之间(下限防止 QC 点太少偶然算成零,上限防止某次 QC 打到包裹体时给出荒谬的大误差)。这正好是 ADEPT 里那个写死的"3% 标定不确定度"想表达的东西——区别在于 DRUID 是从数据里算出来的,而 ADEPT 那个是常数。

为什么必须给出相关系数 ρ

最后还有一个容易被忽略的输出:206Pb/238U 与 207Pb/206Pb 之间的相关系数 ρ(典型值 0.7–0.9)。它们之所以强相关,是因为两者共用同一个 206Pb 测量值——206Pb 测高了,第一个比值偏大、第二个比值偏小。

这个 ρ 在算年龄的时候用不上(两个比值各自换算成年龄),但在画协和度图时必须用:数据点的误差椭圆倾角由 ρ 决定。忽略它,椭圆会被画成"正的",视觉上完全失真,可能让人以为某个点偏离协和曲线,其实它只是沿着协和线方向有一大段误差。

刀切法(jackknife)在这里怎么用

做法很直接:窗口里有 n 次采样,就依次去掉第 j 次、用剩下 n−1 次重算一遍比值,得到 n 个"伪值"。这 n 个伪值的离散程度就是不确定度的估计:

Var = ((n−1)/n) · Σ (x(j) − x̄)²  系数 (n−1)/n 是刀切方差的标准修正因子

为什么不用解析公式?因为刀切法不需要假设噪声的分布形式。前面说过真实噪声里混着泊松成分和闪烁成分,比例还随信号强度变化,解析式很难写对。刀切法直接从数据本身估计离散度,两类噪声都会被算进去。代价是计算量(每个窗口要重算 n 次),但这是值得的。

四、从比值到年龄:一个公式,和三套时钟

到这里,每一个窗口都有了两组数:比值和它的不确定度。剩下的数学反而简单了——因为这部分的物理只有一条定律,没有别的干扰项。

4.1 有解析解的两个:一行公式

把第 1.2 节那个式子反解,就是年龄:

206Pb*/238U = eλ₂₃₈t − 1  ⟹  t = ln(1 + r) / λ₂₃₈

207Pb*/235U = eλ₂₃₅t − 1  ⟹  t = ln(1 + r) / λ₂₃₅

两个实现细节值得提。第一,代码里用 log1p(r) 而不是 log(1+r)——当 r 很小时(年轻样品),这两个在数学上等价但在浮点数里不等价,log1p 专门为这个情况设计,能保住有效数字。

第二,r 可能是负数。扣完气体空白和普通铅之后,净信号是允许在零附近随机波动的(第 3.3 节)。如果 r ≤ −1,对数发散。代码里的处理是把 r 钳到 −0.999999 而不是返回 NaN——因为返回一个接近零的负年龄,比返回 NaN 要好得多:NaN 会在后面的加权平均里"传染",把一个窗口的 NaN 变成一个坪的 NaN,而一个负年龄只会让那个点在平均里被稀释。这是"选择在哪一层承担错误"的典型例子。

4.2 没有解析解的那个:为什么用二分法

207Pb/206Pb 就麻烦了。它的正演公式是两个指数项之比:

207Pb*/206Pb* = (1 / 137.818) × (eλ₂₃₅t − 1) / (eλ₂₃₈t − 1)

要反解 t,需要解一个超越方程——写不出闭合形式。工程上有两条路:用牛顿法数值求根,或者用泰勒级数近似公式。这套代码选的是二分法,理由写得很实在:因为放射性累积是单调的,函数在区间上单调递增,二分法保证收敛;而牛顿法在某些区间会"跳飞"到无意义的解上去。在科研代码里,可预测的慢比不可预测的快更值钱。

实现上,先确定一个包含真解的区间 [1 Ma, 4567 Ma](下界取 1 Ma 是因为方程在 t→0 附近退化,上界取地球年龄),然后每次取中点、判断正演比值偏大还是偏小、收缩区间。R 侧迭代 60 次,Python 侧 80 次——都远超精度需要(区间长度除以 2⁶⁰ 早已小于 1 年),多迭代没有收益。

4.3 三套时钟,各有各的擅长区间

为什么要有三套?因为它们对年龄的灵敏度分布完全不同。

1 Ga 100200500 100020005000 样品真实年龄 (Ma) 125 102050 比值相对 100 Ma 时的倍数 206Pb/238U 全程上涨最快 207Pb/206Pb 在 1 Ga 以内几乎是平的 从 100 Ma 到 1000 Ma: 蓝线涨 10.7 倍,红线只涨 1.5 倍
图 8 三套时钟的相对灵敏度(双对数坐标)。横轴是样品真实年龄,纵轴是"比值相对 100 Ma 时增加了多少"。206Pb/238U(蓝)在整个区间都上涨最快——同样的年龄差异造成更大的比值差异,所以测量误差换算成年龄误差就更小。207Pb/206Pb(红)在年轻端几乎是平的:从 100 Ma 到 1000 Ma 比值只涨了 1.5 倍,一点点测量误差就会被放大成巨大的年龄误差;它要到 2 Ga 以上才开始有分辨力。这就是"年轻样品用 206/238、老样品才用 207/206"的量化依据,也是 ADEPT 的 decay_system = "auto" 在 1 Ga 处自动切换体系的原因。
体系最适用优势弱点
206Pb/238U< 1 Ga信号最强,灵敏度最高依赖 U-Pb 元素分馏校正的好坏
207Pb/235U各个区间都一般与 206/238 独立(用不同的母体)235U 只占 0.72%,信号弱,误差大
207Pb/206Pb> 1 Ga两同位素同为 Pb,对元素分馏不敏感;不依赖 U 含量年轻样品上几乎无分辨力

一个实用推论。对年轻锆石(比如本工具箱最常处理的 200–600 Ma),207Pb/206Pb 年龄通常没什么意义,只能用来做协和度检验。如果看到报告里把 207Pb/206Pb 年龄当成主要结果来引用,那多半是个错误。ADEPT 的 Decay_system 列会明确写出这个坪用的是哪套体系,值得看一眼。

4.4 协和度:三套时钟互相检验

同一个样品的三套年龄理论上应该相等——因为它们是同一个时钟的三个不同读数。实际上它们会有差异,而差异本身就是信息:

协和度 (%) = 207Pb/235U 年龄 ÷ 206Pb/238U 年龄 × 100

为什么会有不协和?最主要的原因是铅丢失:如果晶体后来受过热或变形,一部分放射成因的铅可能扩散丢失。因为不同同位素在不同矿物中的扩散行为不同,铅丢失对三套年龄的影响程度也不一样,于是三个数字就分开了。另一个原因是多期混合——这正是本方法要解决的(第 2 章),而它也会表现为不协和。第三个原因是普通铅扣得不干净(第 3.4 节),未扣净的普通铅会拉低 207Pb/206Pb。

通常的判断标准是:协和度落在 90–110% 之内算协和,可以考虑使用;超出这个范围的点需要解释。

本批匿名示例批次的实测值(48 个样品测点):协和度中位 101.3%,其中落在 90–110% 的占 94%。这个数字说明两件事:这批数据的质量是好的;以及——如果这个比例突然掉到 70% 以下,第一件该怀疑的不是样品,而是前面某一处校正出了问题。

还有一张图在 U-Pb 文献里很常见,这里简单说明它的含义:协和曲线图(concordia diagram)。横轴是 206Pb/238U、纵轴是 207Pb/235U,把所有可能年龄的理论值连成一条曲线。协和的样品点落在这条曲线上;铅丢失的样品点因为两个比值按不同比例降低,会落在曲线下方。这也是为什么前面强调那个相关系数 ρ:数据点的误差椭圆倾角由它决定,画协和曲线图时必须用对。

五、从剖面到坪:在一条抖动的线上找台阶

到这里我们有了:每个测点一条"年龄随深度变化"的剖面,每个点带不确定度。剩下的问题是把这条线读成一到两个年龄。这件事看起来简单——人眼一眼就能看出台阶在哪——但要让程序每次都做出同样的判断,需要把它拆成明确的规则。

5.1 为什么不能整段平均

回到图 2 里的例子:一个坑从上到下穿过了 240 Ma 的边和 400 Ma 的核。如果对整段做一次积分平均,得到 330 Ma——这个数字不对应任何一次地质事件,它是两个人的平均身高。更糟的是它会带着一个看似合理的协和度,让人觉得"这是个有效数据"。用一个不存在的事件去讲一个故事,是这类数据最危险的用法。

所以要找的是剖面里那些稳定的水平段——在钻探行话里叫"坪"(plateau)。一个坪意味着一件事:在这段深度范围内,测到的年龄没有系统性变化,这通常对应晶体生长时的一个稳定阶段。

5.2 分段:PELT 在做什么

"找出水平段"可以写成一道最优化题。假设把序列切成 k 段,每段内部的"不平整程度"用段内残差平方和衡量(就是各点到本段均值的距离平方和):

最小化 Σ各段 (段内残差平方和) + β × (变点个数)

第一项让每段尽可能平,第二项惩罚"切太多段"。第二项不能少:没有它,最优解永远是"每个点自成一段",因为那样每段的残差平方和恰好是零。这个加惩罚的想法和 BIC 准则同源——你想让模型更贴合数据,但要为复杂度付出代价。

求解这个问题的算法叫 PELT(Pruned Exact Linear Time,Killick、Fearnhead & Eckley,2012),ADEPT 里的实现来自 R/pelt.R。它的关键是"剪枝":如果某个候选变点已经不可能成为最优解的一部分,就直接剔除,不必再考虑它。这让平均计算量从 O(n²) 降到接近 O(n)。

代价函数用的是前缀和技巧:要算"第 s 到第 t 个点的残差平方和",不需要遍历这 t−s+1 个点,而是用两个预先算好的累加数组直接相减:

cost(s, t) = (Σx²) − (Σx)² / 点数  两个前缀和之差,O(1)

惩罚项取 (4/100) × log(n)。那个 4 不是随手写的:它来自 changepoint 包对"单变量均值模型"给出的 AIC 惩罚值(2 个参数 × 2)。ADEPT 把这个常数硬编码进来,就不必依赖那个第三方包的特定版本——因为包的版本一变,这个内部常数可能跟着变,而结果会跟着变。

为什么惩罚项里有 log(n)?因为段数随数据长度的合理增长本来是次线性的。如果惩罚是常数,那么序列越长、被切出的假段就越多(噪声总能凑出看起来有用的分段)。加一个随 n 缓慢增长的项,是对"样本更大所以更该谨慎"这件事的量化。

5.3 分段之前要先平滑——以及一个危险的例外

直接对原始剖面跑 PELT 会出问题:噪声本身就有很多局部起伏,算法会把它们当成变点,于是分出大量没有意义的短段。所以标准流程是先用 LOESS(局部加权回归)把剖面磨平一点。

LOESS 的做法是:对每个点,用它周围一段范围内的点做加权线性回归,用拟合值代替原值。span = 0.15 意味着每个点用邻近 15% 的数据来拟合。这样保留了大尺度的趋势(真正的域边界),抹掉了小尺度的抖动(噪声)。

这个平滑步骤有一个必须知道的前提。LOESS 之所以必要,是因为原始剖面里混着剥蚀分馏造成的趋势——那条 20% 的斜率(第 3.7 节)。平滑的任务之一就是吸收它。

但如果喂进来的数据已经做过 F(τ) 分馏校正(比如 DRUID 输出的窗口年龄),剖面本来就是水平的,真实的域边界只是一个台阶,而 LOESS 会把台阶的两侧磨圆。边界正是要找的东西,磨圆之后 PELT 就定位不准了。

所以 ADEPT 有一个开关:smooth = "none"。喂已校正数据时必须打开它。注意这不影响去离群点的那一步——它只是不做平滑,标准化照做(因为 PELT 需要一个统一的尺度)。

5.4 标准化:为了一个技术性的理由

平滑之后,剖面值会被线性缩放到 [0, 1] 区间(min-max 标准化)。这一步不是为了好看,而是因为 PELT 的惩罚项是一个固定数值:如果数据本身的尺度是几十秒的年龄差,惩罚太小不起作用;如果尺度是 0.01,惩罚又过大。把尺度固定下来,惩罚项才有可比的意义。

顺带说明代码里的一个细节:标准化用的是 loess 值的最小/最大值作为参照,而原始年龄列也用同一组参照值来缩放。这样两条曲线(平滑的和未平滑的)落在同一个尺度上,可以直接比较——这正是 smooth = "none" 时它们能完全重合的原因。

5.5 四步筛选:从"切出来的段"到"能用的坪"

PELT 会切出很多段,其中大部分是噪声或过渡带。接下来要过滤。过滤是有顺序的,每一步针对一类特定的"不该要的段":

(上)PELT 把剖面切成段,但只有一部分是真正的坪 480453440 年龄 (Ma) 变点 变点 坪 ✓ 约 453 Ma 太短 → 剔除 坪 ✓ 约 447 Ma 00.20.4 0.60.81.0 归一化深度 τ (下)四步筛选依次作用,每一步针对一类特定的"不该要的段" PELT 切出若干段 ① 年龄范围 0–4540 Ma ② 段内方差 ≤ 0.1192 ③ 积分时长 ≥ 5 s ④ 方向性 → 坪 噪声也会被切出来 抓扣错铅、比值搞反 去掉过渡带 去掉点数太少的段 保证年龄变化讲得通
图 9 上:一条剖面会被 PELT 切成好几段,但只有一部分是真正的坪。中间那段虽然平,但太短(不足 5 秒),会在第三步被剔除;两侧陡峭的部分是过渡带,方差太大,在第二步就出局。下:四步筛选的顺序不能换——先按年龄范围排除明显非法的值(这一步的价值在于抓前序步骤的真错误:负年龄往往意味着普通铅扣错了),再按方差排除过渡带,再按时长排除点数不足的段,最后按方向性保证剩下的段在物理上讲得通。
  1. 年龄范围:坪年龄必须落在 [0, 4540] Ma 之内。这一步看起来多余(年龄怎么可能是负的?),但它会抓到前面步骤的真实错误——比如普通铅扣错、比值列搞反。超出这个范围的段一律丢弃。
  2. 段内方差:把每段的值归一化到 [0,1] 之后算方差,阈值 0.1192。方差大的段是过渡带——它"跨越"两个域,本来就不该被当成坪。这个阈值是从原始发布的流程沿用的经验值,不是从数据推导出来的,属于可调参数。
  3. 最短时长:一段的积分时长必须 ≥ 5 秒。太短的段虽然平,但点数太少、不确定度大,不足以支撑一个年龄。第 1 段豁免这个规则——它带着剥蚀起始的信号,常常被 lower_ablation_time 截断,如果一视同仁就永远会被丢掉。
  4. 方向性:见下一节。这是四个步骤里唯一一个考虑了"年龄之间的相互关系"的。

5.6 方向性:从"全有或全无"到"保留最大的自洽子集"

锆石的深度剖面通常有一个方向性:如果坑从边打向核,年龄应该单调变化(一直增或一直减)。所以那些"和趋势相反"的段可能不可靠。

但这里有个真实的困难:噪声会让局部的顺序偶尔颠倒。三个坪的年龄是 453.0、453.2、453.1 Ma——这显然是一条上升趋势,只是中间那个点略微回落了一点。怎么处理?

v1.1.0 的做法(strict)v1.2.0 之后的默认做法(monotonic)
规则要求完全单调;只要有一处反褶就判定失败求最长单调子序列,允许 2% 的相对反褶
失败时怎么办退化为"第 1 个坪 + 方差最小的那个"——最多两个不存在"失败":总能找出一条最长的自洽链
后果有噪声的数据上,大部分坪会被丢弃保留所有与趋势一致的坪

这个改动是这套工具一个重要的演进:从"全有或全无"变成"保留最大的自洽子集"。算法上,求最长单调子序列是标准的两层循环动态规划(O(n²),但 n 是段数、通常只有几十,代价可以忽略)。

为什么一定要保留方向性判断?因为一个剖面里出现的"台阶"有可能不是地质事件,而是仪器扰动。要求坪之间保持一致的年龄变化方向,是一种利用地质常识来排除随机噪声的手段。filter_direction 参数决定要"递增"(Forward)还是"递减"(Reverse),direction_tolerance(默认 2%)控制允许多大的反褶。

5.7 坪年龄怎么算:两种情形

最后一步是把坪内那些点合成一个年龄。这里有两条路径,取决于输入里有没有不确定度:

输入只有年龄输入带逐点 1σ(Format 4)
坪年龄平滑曲线的段内均值实测年龄的反比方差加权平均
坪不确定度点的散度 ÷ √n1 / √(Σw),其中 w = 1/σ²
额外输出—MSWD、MSWD 概率、参与点数

加权平均的直觉是:误差小、更可信的点应该有更大的话语权。权重取方差的倒数,这样一个 σ 小一半的点,权重会大四倍。理论上这也给了我们一个可以验证的性质:如果所有点的 σ 都一样,权重就均匀,加权平均必须退化成普通的算术平均。实测确实如此——同一批数据用 σ = 0.5 和 σ = 5(都是常数)算出来的坪年龄完全相同(21.80575,差在 1e-10 量级)。这种"退化测试"是验证加权实现有没有写错的最直接办法。

为什么有 σ 的时候要换掉段均值的算法,而不是只"顺便多报一个 MSWD"?因为两种算法的信息来源不同。平滑段均值是在恢复被噪声掩盖的信息——它假设真实剖面是光滑的。而 σ 列本身已经告诉了我们每个点有多可信,权重已经承载了那个信息。再用平滑曲线,等于把 σ 提供的信息糊掉一次。所以有 σ 时直接用实测点的加权平均,这与 DRUID 里 weighted_mean() 的算法也一致——两个工具在这一点上做同一个选择,结果才能直接对照。

六、MSWD:判断"这个坪自洽吗"

6.1 一个必须回答的问题

假设一个坪里有 30 个点,它们的加权平均是 453.06 ± 1.83 Ma。这个数字的可信度取决于一件没有出现在数字里的事:这 30 个点彼此之间一致吗?

有两种完全不同的情形会给出同样的均值和同样的标准误:

情形 B 的均值看起来很精确,但它毫无意义。只看均值和标准误,你无法区分这两个情形。这就是 MSWD 存在的理由。

6.2 公式与直觉

MSWD = Σ wi(xi − x̄)² / (n − 1)  其中 wi = 1/σi²

读这个公式的方式:分子是"实际散布",分母是"如果误差是诚实的,应该有多少散布"。

更严格地看:如果每个点的误差 σᵢ 都是真实可信的、且所有点来自同一个年龄,那么标准化残差 (xᵢ − x̄)/σᵢ 应该服从标准正态分布,它们的平方和服从自由度 n−1 的卡方分布。MSWD 就是这个卡方值除以自由度,期望值恰好是 1。

所以 MSWD 不需要记住公式,只需要记住一句话:它是"实际散布 ÷ 预期散布",理想值 1。

6.3 三种情形分别意味着什么

三个坪:点数相同、均值相同、误差棒长度不同 ① 散布与误差相称 MSWD ≈ 1 深度 → 可以读成一个年龄 ✓ ② 散布远大于误差 MSWD ≈ 8 深度 → 两个年龄,或 σ 给小了 ③ 散布远小于误差 MSWD ≈ 0.1 深度 → σ 给大了,损失分辨力 红色虚线 = 加权平均年龄。三个面板的均值和点数都一样,只有误差棒长度不同 —— MSWD 比较的是"点的散布"与"误差棒的长度"是否相称。
图 10 MSWD 的三种情形。它不是关于年龄的,是关于误差的——所以报告里 MSWD 从来不该单独出现:一个 MSWD = 8 的坪,配上它的散布图,说明的是"这些点不是一个年龄"或者"σ 给小了",而这两种结论需要用别的证据来区分。ADEPT 与 DRUID 都同时给出 MSWD 和它的卡方概率,就是为了避免只看一个数字。
MSWD数学含义实际含义该怎么办
≈ 1散布与误差相称这些点可以被读成一个年龄正常使用
≫ 1散布远大于误差要么坪内真的有两个年龄,要么 σ 被低估了先检查 σ 的来源(是否漏算了外部重现性),再看剖面是否有未识别的结构
≪ 1散布远小于误差σ 被高估了结果"过于保守",不致命但会损失分辨力

"MSWD ≫ 1"是最常见的,而且多半不是地质原因。一个只统计了计数误差、没有算外部重现性的 σ,通常会偏小两到三倍;平方之后 MSWD 就偏大四到九倍。这是新手报告里 MSWD = 8 的头号原因,而解决办法在 3.8 节——把外部重现性加到方差里。DRUID 用监控标样实测的散度反推这一步,所以它的 σ 从一开始就包含了两项。

6.4 概率读法:比 1 大多少才算大

MSWD 等于 1.2 算不算问题?取决于有几个点。所以除了 MSWD 本身,还应该给出概率:

p = P(χ²n−1 > MSWD × (n−1))  在"只有一个年龄"的前提下,看到至少这么大盘散的概率

常规的警示线是 p < 0.05。这个概率的含义要说清楚:它不是"这个年龄有 95% 的概率是对的",而是"如果这些点真的来自同一个年龄且误差可信,那么出现这么大离散的概率只有 5%"。它是对一致性假设的检验,不是对年龄本身的检验。

一个实现上的坑,值得单独记下来。ADEPT 用的是 R 的 pf(mswd, n−1, Inf, lower.tail = FALSE),而 DRUID 用的是自己实现的卡方上尾函数。这两个必须等价:

pf(mswd, n-1, Inf, lower.tail=FALSE)  ==  chi2_sf(mswd * (n-1), n-1)
                                            ↑ 注意要乘自由度

漏掉那个乘法,概率会算得偏大一大截。两个工具在这一点上对齐过:用 R 生成 153 组 (自由度, MSWD) 网格做对照,最大绝对差 3.2e-15——机器精度。

6.5 一个我们必须承认的局限:重叠窗口

MSWD 的推导假设各个点互相独立。而我们的剖面点不独立——第 3.6 节说过,窗口宽 4 秒、步长 1 秒,相邻窗口共享 3 秒的数据。

这会让 MSWD 系统性偏小,我们可以估算偏差有多大。假设相关系数 ρ ≈ 0.75(共享 3/4 数据),则有效点数约为:

n有效 ≈ n / (1 + 2ρ·(重叠比例)) ≈ n / 2.5

于是 DRUID 内部用的一条判据 MSWD < 1 + 2√(2/(k−1))(用来决定一段是"真实年龄域"还是"过渡带")会偏宽松:

窗口数 k判据门槛若用有效点数
30(实际窗口数)1.531.85(k_eff ≈ 12)

方向是漏杀而非错杀——该被剥掉的混合窗口没剥干净,但不会把好的域误判成坏的。这个偏差在我们全部报告里都存在,属于已知且已量化的局限,不是错误。诚实地说:ADEPT 报出的 MSWD 也应该按这个方向理解——它偏乐观。

但在实际数据上,这个偏差没有大到让结论失效。我们对整个匿名示例批次跑了端到端:48 个样品测点产出 51 个坪,MSWD 中位数 1.25(下四分位 0.62、上四分位 1.71)。如果 σ 被系统性低估,这个中位数会是 3、5 甚至 10,而不是 1.25。所以结论是:DRUID 给出的逐点不确定度基本是诚实的,而重叠窗口那个偏差把真值往下拉了一点点,也在预期之内。

6.6 这两个工具在这件事上的分工

最后值得一提的是:MSWD 在这两个工具里的角色并不相同。

DRUIDADEPT
MSWD 的用途参与判断:决定一段是"真年龄域"还是"过渡带"(refine_domains() 用它逐层剥掉混合窗口)用于报告:坪已经选出来了,MSWD 是随坪一起给出的质控量
计算层级深度剖面域(整段划分出的域)坪(经过四步筛选后的段)
结果表里的列名MSWD / MSWD_概率MSWD / MSWD probability

算法是一致的(都是反比方差加权平均 + 卡方上尾概率),所以两边的数字可以直接并列对照。但它们回答的问题不同:DRUID 问"这个深度范围内是不是一个年龄",ADEPT 问"选出来的这个坪自不自洽"。两个都通过,才说明这个年龄值得写进文章。

6.7 为什么"整段平均"不是一个年龄

到这里有一个自然而然的疑问:既然每个窗口都能算一个年龄,那为什么还要挑坪、分域? 把所有窗口平均一下不就行了?这个问题值得正面回答——它正是"逐深度"这套做法存在的理由, 也是后面那张"不分域年龄"表存在的理由。

先澄清一个容易混的地方:"整段"其实有两种算法,它们不是一回事。 一种是把整段的全部计数合起来、算一个比值再代入年龄方程(下面叫"整段积分",结果表的 年龄206_238 就是它);另一种是每个窗口各算一个年龄,再按 1/σ² 加权平均。 两者在单一年龄的测点上很接近,但在真有结构的测点上可以差 20% 以上——因为它们对"哪一段计数更重要" 的取舍不同:积分让计数率高的窗口占更大权重,加权平均让年龄不确定度小的窗口占更大权重。 不要把它们当成同一个数。

再回到那个关键问题。把整段当一个域做加权平均,得到的确实还是一个数, 但这个数有没有意义,取决于一件事:整段的 MSWD 是不是大约 1。 如果剖面本来就有两个年龄(外圈老内圈新,或者反过来),加权平均出来的值落在两者之间, 它不对应任何一个真实时刻——它是一个混合值。而 MSWD 会把这个事实报出来。

实测(匿名示例批次):48 个样品测点里,整段与"只有一个年龄"相容的只有 5 个(约 10%), 整段 MSWD 的中位数是 4.44,最大到 196。也就是说,九成的测点如果跳过"分域"这一步, 拿到的就不是年龄。这就是"深度剖面"不能被整段平均替代的量化理由。

那为什么还要专门出一张"不分域"的表?因为对比需要同一把尺子。 分域之后,每个测点得到的域数、域的位置都不一样(示例批次里 33 个点在 2~3 个域之间,另 15 个是均一的), 测点之间没法直接比;而"不做任何分域"是所有测点都走同一条路径,于是它就是那把尺子—— 整段值、分域主域、整段积分三个数并排,两级之间的差额精确地告诉你"这一步动了多少"。 把这三步的差异摊开,比只报告最后一个数更容易发现自己在哪一步被骗了。

七、结果表里每个数是什么意思

前面六章讲了数字是怎么算出来的。这一章反过来:拿到一个结果文件,从哪里开始看。

7.1 DRUID 的输出:六张表

表名粒度看什么
结果每个测点一行先看这张。整体质量、协和度、哪些点有深度结构
标样QC每个标样一行第二重要。这批数据的校准到底行不行
深度剖面域每个域一行检出了多域的测点,逐个域列出深度范围与年龄
不分域年龄每个样品测点一行不做任何分域时的年龄基准。与分域主域、整段积分三种算法并排,差额就是"分域"这一步的贡献(见 6.7 节)
剖面窗口每个窗口一行逐窗口剖面,列名按 ADEPT 的输入约定对齐,直接喂过去即可
运行参数一行一项本次运行的全部参数与跳过的文件。半年后回溯时全靠它

结果表里的关键列

列名含义怎么读
类型样品 / 主标 / 监控标样 / 玻璃标样角色由样品名自动判定。看统计前先确认这一列
U238_cps剥蚀段的平均 238U 计数率太低(< 10⁴)说明这个点信号弱,误差会大
Th_UTh/U 比(灵敏度未校正的表观值)岩浆锆石通常 0.1–1.5;明显偏离要考虑包裹体或变质成因
f206_pct普通铅占总 206Pb 的百分比质控指标。正常应接近 0;超过几个百分点说明这个点被普通铅污染得厉害
Pb206_238U比值本身配合下一列一起看
s68_pct上面那个比值的相对 1σ(%)< 2% 算好;> 5% 说明这个点基本没有分辨力
年龄206_238206Pb/238U 年龄年轻样品的主要结果
s68_1sig年龄的 1σ给下游算 MSWD 必须用这一列
s68_2sig年龄的 2σ写文章报告不确定度时常用 2σ,但不能拿去算 MSWD
年龄207_206207Pb/206Pb 年龄对年轻样品通常无意义,只用于协和度检验
协和度_pct207Pb/235U 年龄 ÷ 206Pb/238U 年龄 × 10090–110% 内算协和
深度结构均一 / 多域(N) / 空空表示没有可分析的深度剖面;多域表示检出多个年龄域

s68_1sig 和 s68_2sig 并排放着,拿错那一列会让 MSWD 差 4 倍。这不是夸张:MSWD 的分子里 σ 是平方的,用 2σ 代替 1σ 会让 MSWD 缩小到四分之一。这个错误不会报错、不会警告,只会给出一个"看起来很好"的 MSWD ≈ 0.25。给下游传数据时,一定要传 _1sig。

7.2 把剖面画出来:一个真实测点

一个真实测点的深度剖面:35 个窗口的年龄、两个被识别出的坪、以及被剔除的过渡带
图 11 一个真实测点的深度剖面(匿名示例批次中的 S01 测点,35 个窗口)。灰点是逐窗口的年龄与 1σ 误差棒,绿线是 ADEPT 平滑后的剖面。两个紫色区间是 ADEPT 找出的坪,中间的红色区间是被剔除的过渡带——它跨越了约 490 Ma 的域,年龄在那里快速变化,方差过大。这张图是理解整套方法最快的方式:一眼能看出为什么"整段平均"没有意义,以及为什么"随深度分辨"能救回两段数据。

7.3 ADEPT 的输出

ADEPT 返回两个表(以及可导出的 Excel):

列名含义
Points这个坪用了多少个窗口点
Start / End坪在时间轴上的起止(秒)
Final age (Ma)坪年龄。有 σ 输入时是加权平均,否则是平滑曲线的段均值
Final total uncertainty (Ma)坪年龄的总 1σ:随机项与标定项正交合成
... incl. decay (Ma)再把衰变常数本身的不确定度算进去。做区域对比、跨实验室对比时用这个
Random / Systematic uncertainty上面两部分的分解,便于看哪一项占主导
Decay system这个坪用的是哪套体系(Age68 或 Age76)。1 Ga 以下基本都是 Age68
Relative uncertainty (%)总不确定度的相对值
MSWD / MSWD probability见第六章。无 σ 输入时为 NA
Points with uncertaintyMSWD 实际用了多少个点(可能有几个 σ 非法的点被剔除)
Confirmed plateaus这个测点一共确认了几个坪
Concordance (%)协和度

7.4 三个判据:这批数据能不能用

每次拿到一批结果,先看这三个数字。三项都过,数据可用;任何一项不过,先查原因而不是先改结论。

判据合格线不合格时先怀疑什么
91500 主标的偏差绝对值 < 1%标定环节。可能是标样点剥蚀异常、或夹逼标样选得不好
监控标样(Ple)的偏差绝对值 < 3%外部重现性。这个数字直接决定样品的 σ 是否可信
协和度落在 90–110% 的样品占比> 85%普通铅校正(3.4/3.5)或分馏校正(3.7)

匿名示例批次的实测值(48 个样品测点 / 1677 个窗口):

指标实测判定
91500 偏差−0.31%合格(< 1%)
Ple 偏差+1.85%合格(< 3%)
协和度中位101.3%,90–110% 占 94%合格(> 85%)
样品 206Pb/238U 年龄中位458.1 Ma(5–95%:420.2 ~ 643.6)—
ADEPT 坪数 / MSWD 中位51 个坪 / 1.25健康(围绕 1)

这三个判据的共同点是:它们都是"内部一致性"检查,不能证明年龄是对的。它们能发现的是"这批数据内部有问题"。真正的正确性要靠外部证据——与已发表年龄的对照、不同方法的交叉验证、地质关系的合理性。软件能保证的只是"没有算错",不能保证"结论是对的"。这个界限在第八章还会再讲一次。

八、我们怎么知道这些数字是对的

这一章是这份文档存在的主要理由。前面七章解释了每一步"应该"怎么做,但一个流程最难的部分不是设计它,而是确认它真的在按设计运行。特别是当它开始产出看起来合理的数字之后。

8.1 锚点:不许悄悄改数

这套工具最核心的防线是把已经核对过的结果硬编码进测试。具体说:

# tests/testthat/test-adept.R —— 每次运行都会被检查
expect_equal(ages, c(21.61723849, 22.56383385, 23.60464105))

这三个数字来自项目的早期版本,是用真实数据一次一次核对出来的。它们的意义不是"正确"(没有任何单一数字能证明正确),而是"没有变"。任何重构、优化、参数调整,只要动了任何一个数值计算环节,这三个数字就会变,测试立刻失败。

这条防线在这次开发中真的起作用了。我们做了三次较大的重构(拆分 700 行的主函数、把 20 万次循环改成向量化、加入不确定度加权),每次都靠这个锚点确认"默认路径一个数没动"。有一次改动确实让锚点变了——那正是它抓到的一个真实 bug(见 8.4 节第 6 条)。

8.2 两套独立实现互相对照

U-Pb 的计算在 R 和 Python 里各写了一遍(ADEPT 用 R,DRUID 用 Python),这不是浪费——它们可以互相做交叉验证。

有价值的对照发生在统计函数上,因为这类函数最容易"看起来对"却差一点点:

量R 侧实现Python 侧实现对照结果
卡方上尾概率pf(x, df, Inf, lower.tail=FALSE)自实现的不完全 Gamma 函数153 组网格,最大绝对差 3.2e-15
加权平均与 MSWDseg_weighted_mean()weighted_mean()算法一致(同一个公式)
年龄方程二分法 60 次二分法 80 次收敛到同一解

还有一个独立的第三方对照:PELT 分段的实现与 R 的 changepoint 包做过比对(在 n = 60/161/411、1–6 个真实变点的多组序列上,变点集合完全一致),之后才把那个依赖去掉。去掉的理由是:第三方包的内部常数可能随版本变化,而结果不该跟着变。

8.3 端到端:用真实数据跑完整条链

单元测试只能保证"每个零件是对的",不能保证"装起来是对的"。所以还有一个端到端检查,跑的是仓库里自带的匿名示例批次:

python tests/check_example_batch.py

它会完整跑一遍 85 个测点(约 1 分钟),然后逐项核对:各表的行数、标样 QC 的偏差、样品年龄的分布、深度结构的分布,以及不分域整段结果的判定。这些期望值都硬编码在测试文件里,和 8.1 节的锚点同样的道理。

这个检查在 CI 上也会跑——所以每次代码改动,都会有一台干净的机器用真实数据验证一遍数值没变。

为什么"匿名示例批次"这件事本身就是一种验证。把真实批次的样品名替换成顺序编号(S01…S48,标样名保留)之后,我们把匿名批次完整跑了一遍,与原批次逐单元格对比:所有表的共有列都完全一致,只有样品名、文件名、路径这三列不同。这同时证明了两件事:匿名化没有动任何数值;以及"同样的输入必然给出同样的输出"这条性质是成立的。

8.4 已知的局限:一份诚实清单

一份只讲优点的文档没有价值。下面是这套流程已知的、我们没打算隐瞒的问题:

#局限影响状态
1重叠窗口让 MSWD 偏乐观(3.6 / 6.5 节)有效点数约为标称的 1/2.5,MSWD 系统性偏小已量化,未修正(修法有两条,都不便宜)
2死时间校正默认关闭高计数率下 Pb/U 被轻微抬高,年龄略偏老设计决定,为保持与历史结果可比
3207Pb/235U 的不确定度是近似传播把 206/238 的相对误差原样搬过来,未严格处理协方差算法自早期版本未变,为保持可比
4部分筛选阈值是经验值(方差阈值 0.1192、方向容差 2%)不是从数据推导的,可能对不同仪器不是最优沿用原始流程;adept_sensitivity() 可以扫参数看结论稳不稳
5没有与 ID-TIMS 的绝对对照能验证内部一致性,不能验证绝对准确度需要外部标样比对实验
6jackknife 本身也有假设它假设各次采样可交换;如果有系统性时间趋势,会低估误差已用 smooth="none" + F(τ) 处理趋势

第 6 条背后有一个这次开发中发生的真实故事,值得记下来。在给剖面的加权平均做重构之后,锚点年龄变了,而且变化的方式很微妙:剖面里完全平坦的段会得到"未定义"的方差,于是被静默丢弃。原因是一段数学上正确的链条:段内归一化时,零极差的段返回 NaN;NaN 经过一个 pmax 变成 NA;NA 让方差检查返回缺失;于是整段被当作"不合格"扔掉。

而"完全平坦的段"恰恰是最好的坪——它只会在一种输入上出现:做过分馏校正、又关掉了平滑(即 smooth="none")的数据。这正是为 MSWD 准备的那种输入。换句话说,一个数学上正确的中间步骤,把最重要的一类数据静默地丢掉了,而且丢得没有任何提示。这个 bug 是被锚点测试抓到的;如果没有锚点,它会带着"结果略少几个坪"的症状一直存在下去。

一个体会。这类问题比"程序崩溃"危险得多——崩溃会告诉你出错了,而静默丢弃只会让你看到一个稍小的数字,然后合理地解释它("这批数据质量不太好")。所以每次重构之后,我们都坚持跑一遍端到端对照,而不只是看单元测试过没过。

8.5 我们验证不了什么

最后要划清一条线。以上所有验证,保证的都是"计算过程没有错",不是"这个年龄是对的"。

这也是为什么第 7.4 节三个判据的措辞是"内部一致性检查"。软件的责任是把每一步做对、把不确定度诚实地说出来、并且在可能的时候提醒你哪里可疑。判断这个年龄意味着什么,永远是人的工作。

九、如果你要参与

这套工具是在解决一个具体问题时长出来的,所以它的结构反映的是问题的形状,而不是某种"优秀架构"。这意味着它有真实的缺口,而且缺口的位置是可预测的。下面按"需要的技术门槛从低到高"排列。

9.1 最有价值的贡献:报告一个说不通的结果

这件事不需要写代码,但对项目的价值可能超过任何代码贡献。因为开发者在自己的数据上永远看不到别人数据里的毛病。

一个有用的报告长这样:

  1. 哪一步出了问题。是 DRUID 的还原阶段("标样 QC 看起来不对"),还是 ADEPT 的分段阶段("坪切得位置不对")?
  2. 期望是什么、实际是什么。"这个测点的剖面在 15–25 秒之间明显是平的,但 ADEPT 没有在这里切出坪" 比 "结果不对" 有用一百倍。
  3. 具体数值。哪个测点、哪一秒、期望多少、实际多少。如果是数值差异,请说明差多少量级(1% 和 10 倍是完全不同的问题)。
  4. 能不能最小复现。如果只用一个测点就能重现,问题就解决了一半。

什么类型的报告最有价值?不是"程序崩溃了"(那种问题会自己暴露),而是"它跑完了、结果看起来正常、但我觉得不对"。这类问题需要人的判断才能发现,也最容易被忽略——因为软件本身不会觉得哪里奇怪。8.4 节那个"平坦的段被静默丢弃"就是这类问题,它潜伏了很久,直到有人去核对一个本以为无关的数字。

9.2 加数据:不同仪器、不同批次、已知年龄的样品

这套流程的很多参数是从本实验室的单台仪器(iCAP RQ)和有限批次上标定出来的:搜索窗口宽度、剥蚀定位的阈值倍数、方差阈值、外部重现性的上下限。它们在别的仪器上大概率需要调整。

所以下面这些数据特别有价值:

加数据的时候有一件事必须做:如果数据要进仓库,先匿名化。做法在 AGENTS.md 里写着——样品名替换成顺序编号,但标样名必须保留(它们是国际标准物质,而且角色判定靠名字匹配,改了名流程就找不到主标了)。

9.3 加测试:一个我们自己踩过的坑

写测试的时候,最容易犯的错误是凭印象写出一个"应该是"的值。这件事发生在我们身上,值得详细讲。

在给卡方上尾概率写测试时,我"记得"χ² = 200、自由度 10 的概率大约是 3.0559×10⁻³⁸,就把它写进了断言。测试立刻失败,实测值是 1.6139×10⁻³⁷——差了五倍。

这时候有两种反应:去改实现(因为"测试说不对"),或者去核对参考值。我们去 R 里算了一遍:R 给出的正是 1.6139305336977309×10⁻³⁷。也就是说实现是对的,是我编的期望值错了。

教训:测试里的期望值必须来自可信来源。来自另一个独立实现、来自解析解、来自已发表的数据——什么都行,就是不能来自"我觉得是这个数"。

因为凭印象写的期望值比没有测试更糟:它会推着你去"修"一个本来正确的实现,把一个好代码改成错的。而如果那次没有去核对参考值、而是直接改实现,这个 bug 会带着"测试全绿"的假象进入发布版本。

同样的原则也适用于加新的端到端检查:期望值要么来自同一批次的历史输出,要么来自手工验证过的计算。测试的价值完全取决于它的期望值有多可信。

9.4 加代码:先看这两份文件

如果要在代码层面动手,有两个文件必须先读,因为它们记录的是读代码看不出来的东西:

文件内容
AGENTS.md(DRUID 仓库)怎么验证、哪些数字不许动、常见的坑在哪。包括"R_LIBS_USER 必须 export,否则会加载到旧版本"这种花了几个小时才发现的事
AGENTS.md(ADEPT 仓库)同上,外加两个工具之间的术语对照表(plateau / domain,MSWD probability / MSWD_prob)

改完之后必须做的三件事:静态检查通过、自检通过、以及跑一次端到端对照确认数值没变。第三条不能省——它才是真正在保护这套东西的那一条。

9.5 关于"开源能带来什么"

这份文档最后想说一件不完全是技术的事。

这套工具的价值不取决于它有多聪明,而取决于有多少人真的用它、并且把遇到的问题说出来。原因很直接:软件的缺口是由使用方式决定的,而使用方式是开发者想象不出来的。

举两个真实的例子,都来自这次开发过程:

这两个都不是"代码写得不好",而是"没人想到会这样用"。用户反馈在这里不是帮开发者找 bug,而是帮开发者看到自己没看到的用例。

所以如果你在用这套东西,遇到任何说不通的地方——哪怕只是"我觉得这个数字怪怪的"——那都值得说出来。这份文档里所有的坦承(包括 8.4 节那份局限清单)都是为了让这种交流更容易发生:承认自己不知道,才有人愿意告诉你答案。

最后回到那个公式。整份文档讲了很多东西,但核心只有一行:

t = ln(1 + 206Pb*/238U) / λ238

其余七千行代码,全部在做同一件事:在把这个比值测出来之前,把污染它的东西一样一样拿走,并且说清楚还剩多少不确定性。没有一步是聪明的,每一步都是必要的。

关于这份文档 所有公式、常数与阈值均取自源码(druid/ 与 ADEPT/R/),实测数值来自匿名示例批次 examples/EX2022A(48 个样品测点 / 1677 个窗口)。示例数据随仓库发布,所以这里面的每个数字你都可以自己跑一遍复现。

关于产生方式 正文由 AI(DeepSeek V4.1)协助撰写:章节结构、图解与行文出自它;其中的公式、常数与全部实测数值,均对照源码与随仓库发布的示例批次逐条核对过,出处标在文中。任何一处可疑都请到 仓库 issue 提出——这份文档值得被挑错的地方,正是它存在的原因。

相关文档:站点首页 · DRUID 与 ADEPT 的联系与数据流(两个工具的分工、边界上传递什么) · 各仓库的 README.md(安装与用法) · 各仓库的 AGENTS.md(开发须知)