From b06ebb2975725d4956ce74851b83ed4ee954a286 Mon Sep 17 00:00:00 2001 From: Maarten Marsman Date: Wed, 21 Oct 2026 17:07:19 +0200 Subject: [PATCH] test(nuts): bound the divergence rate instead of requiring exactly zero NUTS trajectories differ across platforms: the M.2F block in test-mixed-nuts.R has one divergent transition on the Linux x86 runner and none on arm64, with identical seeds, and the weekly certification job has failed on it every Sunday since 2026-08-09. The sampler-agreement comparisons in that block pass on both. The ten exact-zero assertions in test-mixed-nuts.R and test-ggm-nuts.R now require the divergence rate the diagnostics already compute to stay below 0.1% of post-warmup transitions. --- tests/testthat/test-ggm-nuts.R | 8 ++++++-- tests/testthat/test-mixed-nuts.R | 20 ++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/testthat/test-ggm-nuts.R b/tests/testthat/test-ggm-nuts.R index 9e35cb3f..29cb3358 100644 --- a/tests/testthat/test-ggm-nuts.R +++ b/tests/testthat/test-ggm-nuts.R @@ -10,6 +10,10 @@ # 1. Posterior moment comparison (means, variances, KS, bivariate) # 2. Edge selection accuracy (PIPs) # 3. Gradient near PD boundary +# +# Divergences: NUTS trajectories differ across platforms, so the blocks bound +# the divergence rate (below 0.1% of post-warmup transitions) rather than +# requiring exactly zero. # 4. NUTS diagnostics are well-behaved # # TODO: add a proper Geweke JDT or SBC test for one-transition correctness @@ -189,7 +193,7 @@ test_that("NUTS posterior means agree on p=6 tridiagonal", { expect_lt(max(diff), 0.05, label = "max abs diff in pairwise means < 0.05") # NUTS should have clean diagnostics - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -373,7 +377,7 @@ test_that("NUTS diagnostics are clean for well-specified model", { ) expect_true(!is.null(fit$nuts_diag)) - expect_equal(fit$nuts_diag$summary$total_divergences, 0) + expect_lt(fit$nuts_diag$summary$divergence_rate, 1e-3) expect_equal(fit$nuts_diag$summary$max_tree_depth_hits, 0) expect_gt(fit$nuts_diag$summary$min_ebfmi, 0.3) }) diff --git a/tests/testthat/test-mixed-nuts.R b/tests/testthat/test-mixed-nuts.R index 4bee0149..03afc1f2 100644 --- a/tests/testthat/test-mixed-nuts.R +++ b/tests/testthat/test-mixed-nuts.R @@ -11,6 +11,10 @@ # E conditional PL, edge selection, interleaved variable order # F conditional PL, no edge selection, interleaved variable order # +# Divergences: NUTS trajectories differ across platforms, so the blocks bound +# the divergence rate (below 0.1% of post-warmup transitions) rather than +# requiring exactly zero. +# # Weekly certification tier (T2, BGMS_RUN_CERTIFICATION): a full NUTS-vs-MH # condition grid, and each condition fits two # chains x 5000 iterations for both samplers. @@ -176,7 +180,7 @@ test_that("M.2A: NUTS vs MH agree (conditional PL, no ES, grouped)", { diff = abs(fit_nuts$posterior_mean_pairwise - fit_mh$posterior_mean_pairwise) expect_lt(max(diff), 0.1, label = "M.2A max abs diff in associations < 0.1") - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -211,7 +215,7 @@ test_that("M.2B: NUTS vs MH agree (marginal PL, no ES, grouped)", { compare_pairwise_samples(fit_nuts, fit_mh, "M.2B") compare_main_samples(fit_nuts, fit_mh, "M.2B") - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -256,7 +260,7 @@ test_that("M.2C: NUTS vs MH agree (conditional PL, ES, grouped)", { label = "M.2C PIP correlation > 0.95" ) - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -300,7 +304,7 @@ test_that("M.2D: NUTS vs MH agree (marginal PL, ES, grouped)", { label = "M.2D PIP correlation > 0.95" ) - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -377,7 +381,7 @@ test_that("M.2E: NUTS vs MH agree (conditional PL, ES, interleaved)", { colnames(extract_pairwise_interactions(fit_mh)) ) - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -431,7 +435,7 @@ test_that("M.2F: NUTS vs MH agree (conditional PL, no ES, interleaved)", { colnames(extract_pairwise_interactions(fit_mh)) ) - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) }) @@ -591,7 +595,7 @@ test_that("M.2J: NUTS diagnostics are clean for mixed MRF", { ) expect_true(!is.null(fit$nuts_diag)) - expect_equal(fit$nuts_diag$summary$total_divergences, 0) + expect_lt(fit$nuts_diag$summary$divergence_rate, 1e-3) expect_equal(fit$nuts_diag$summary$max_tree_depth_hits, 0) expect_gt(fit$nuts_diag$summary$min_ebfmi, 0.3) }) @@ -635,5 +639,5 @@ test_that("M.2T: NUTS vs MH agree under tilt (conditional PL, no ES, delta=1)", label = "M.2T max abs diff in associations < 0.1 (delta=1)" ) - expect_equal(fit_nuts$nuts_diag$summary$total_divergences, 0) + expect_lt(fit_nuts$nuts_diag$summary$divergence_rate, 1e-3) })