owalnuts 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. owalnuts-0.2.0/CHANGELOG.md +808 -0
  2. owalnuts-0.2.0/Cargo.lock +305 -0
  3. owalnuts-0.2.0/Cargo.toml +71 -0
  4. owalnuts-0.2.0/LICENSE +21 -0
  5. owalnuts-0.2.0/LICENSE.txt +21 -0
  6. owalnuts-0.2.0/NOTICE +8 -0
  7. owalnuts-0.2.0/PKG-INFO +285 -0
  8. owalnuts-0.2.0/README.md +464 -0
  9. owalnuts-0.2.0/SECURITY.md +11 -0
  10. owalnuts-0.2.0/THIRD_PARTY.md +29 -0
  11. owalnuts-0.2.0/examples/funnel_kernel_options.rs +168 -0
  12. owalnuts-0.2.0/examples/funnel_orbit_trace.rs +510 -0
  13. owalnuts-0.2.0/examples/funnel_paper_adaptation.rs +132 -0
  14. owalnuts-0.2.0/examples/funnel_replay.rs +533 -0
  15. owalnuts-0.2.0/examples/gaussian.rs +90 -0
  16. owalnuts-0.2.0/examples/kernel_bench.rs +350 -0
  17. owalnuts-0.2.0/examples/kernel_efficiency.rs +557 -0
  18. owalnuts-0.2.0/examples/readme_quick_start.rs +35 -0
  19. owalnuts-0.2.0/examples/state_space_path_metric.rs +222 -0
  20. owalnuts-0.2.0/examples/structured_metric_benchmark.rs +57 -0
  21. owalnuts-0.2.0/examples/support/reference_nuts.rs +338 -0
  22. owalnuts-0.2.0/integrations/bridgestan/Cargo.lock +454 -0
  23. owalnuts-0.2.0/integrations/bridgestan/Cargo.toml +33 -0
  24. owalnuts-0.2.0/integrations/bridgestan/README.md +164 -0
  25. owalnuts-0.2.0/integrations/bridgestan/artifacts/bridgestan-benchmark.json +271 -0
  26. owalnuts-0.2.0/integrations/bridgestan/models/eight_schools.data.json +5 -0
  27. owalnuts-0.2.0/integrations/bridgestan/models/eight_schools.stan +19 -0
  28. owalnuts-0.2.0/integrations/bridgestan/models/local_level.stan +18 -0
  29. owalnuts-0.2.0/integrations/bridgestan/src/bin/bench.rs +408 -0
  30. owalnuts-0.2.0/integrations/bridgestan/src/bin/wallgap.rs +192 -0
  31. owalnuts-0.2.0/integrations/bridgestan/src/lib.rs +2199 -0
  32. owalnuts-0.2.0/integrations/bridgestan/tests/load.rs +373 -0
  33. owalnuts-0.2.0/integrations/bridgestan/tests/nonfinite.rs +117 -0
  34. owalnuts-0.2.0/integrations/python/.gitignore +6 -0
  35. owalnuts-0.2.0/integrations/python/Cargo.lock +640 -0
  36. owalnuts-0.2.0/integrations/python/Cargo.toml +33 -0
  37. owalnuts-0.2.0/integrations/python/LICENSE.txt +21 -0
  38. owalnuts-0.2.0/integrations/python/README.md +236 -0
  39. owalnuts-0.2.0/integrations/python/src/lib.rs +1616 -0
  40. owalnuts-0.2.0/integrations/python/tests/check_cmdstan_export.py +68 -0
  41. owalnuts-0.2.0/integrations/python/tests/test_adapters.py +496 -0
  42. owalnuts-0.2.0/integrations/python/tests/test_stan.py +143 -0
  43. owalnuts-0.2.0/oracle/walnutpie/f5bba365/README.md +33 -0
  44. owalnuts-0.2.0/oracle/walnutpie/f5bba365/SHA256SUMS.txt +4 -0
  45. owalnuts-0.2.0/oracle/walnutpie/f5bba365/gaussian_cases.json +195 -0
  46. owalnuts-0.2.0/oracle/walnutpie/f5bba365/span_traces.json +14 -0
  47. owalnuts-0.2.0/oracle/walnutpie/f5bba365/transition_sequence_traces.json +4 -0
  48. owalnuts-0.2.0/oracle/walnutpie/f5bba365/transition_traces.json +9 -0
  49. owalnuts-0.2.0/oracle/walnutpie/f5bba365_funnel_leaves/README.md +40 -0
  50. owalnuts-0.2.0/oracle/walnutpie/f5bba365_funnel_leaves/SHA256SUMS.txt +2 -0
  51. owalnuts-0.2.0/oracle/walnutpie/f5bba365_funnel_leaves/funnel_leaf_cases.json +1 -0
  52. owalnuts-0.2.0/oracle/walnutpie/f5bba365_funnel_leaves/generate_funnel_leaves.cpp +126 -0
  53. owalnuts-0.2.0/oracle/walnutpie/f5bba365_invalid_leaves/README.md +46 -0
  54. owalnuts-0.2.0/oracle/walnutpie/f5bba365_invalid_leaves/SHA256SUMS.txt +2 -0
  55. owalnuts-0.2.0/oracle/walnutpie/f5bba365_invalid_leaves/generate_invalid_leaves.cpp +166 -0
  56. owalnuts-0.2.0/oracle/walnutpie/f5bba365_invalid_leaves/invalid_leaf_cases.json +1 -0
  57. owalnuts-0.2.0/pyproject.toml +66 -0
  58. owalnuts-0.2.0/python/owalnuts/__init__.py +1072 -0
  59. owalnuts-0.2.0/src/diagnostics.rs +1027 -0
  60. owalnuts-0.2.0/src/export.rs +318 -0
  61. owalnuts-0.2.0/src/kernel.rs +6167 -0
  62. owalnuts-0.2.0/src/lib.rs +24 -0
  63. owalnuts-0.2.0/src/oracle_tests/funnel_leaf.rs +162 -0
  64. owalnuts-0.2.0/src/oracle_tests/invalid_leaf.rs +234 -0
  65. owalnuts-0.2.0/src/oracle_tests/macro_leaf.rs +216 -0
  66. owalnuts-0.2.0/src/oracle_tests/mod.rs +6 -0
  67. owalnuts-0.2.0/src/oracle_tests/sequence.rs +478 -0
  68. owalnuts-0.2.0/src/oracle_tests/span.rs +329 -0
  69. owalnuts-0.2.0/src/oracle_tests/transition.rs +350 -0
  70. owalnuts-0.2.0/src/sampler.rs +1221 -0
  71. owalnuts-0.2.0/src/types.rs +20 -0
  72. owalnuts-0.2.0/src/walnutpie/research.rs +1226 -0
  73. owalnuts-0.2.0/src/walnutpie.rs +13654 -0
  74. owalnuts-0.2.0/tests/chain_rescue.rs +915 -0
  75. owalnuts-0.2.0/tests/data/arviz_fixture.json +6506 -0
  76. owalnuts-0.2.0/tests/data/generate_arviz_fixture.py +74 -0
  77. owalnuts-0.2.0/tests/diagnostics_arviz.rs +64 -0
  78. owalnuts-0.2.0/tests/export_cmdstan.rs +212 -0
  79. owalnuts-0.2.0/tests/freeze_mode.rs +177 -0
  80. owalnuts-0.2.0/tests/kernel_fingerprint.rs +227 -0
  81. owalnuts-0.2.0/tests/nonfinite_position_policy.rs +142 -0
  82. owalnuts-0.2.0/tests/public_facade.rs +2705 -0
  83. owalnuts-0.2.0/tests/sampler_api.rs +883 -0
  84. owalnuts-0.2.0/tests/structured_refresh.rs +633 -0
@@ -0,0 +1,808 @@
1
+ # Changelog
2
+
3
+ All notable changes to oWALNUTS are documented here. Kernel behaviour is
4
+ identified by `owalnuts::walnutpie::ALGORITHM_REVISION`; a seed reproduces a
5
+ run only under the same revision, crate build, lock file, target
6
+ architecture and operating system (see the `walnutpie` module documentation).
7
+
8
+ ## [0.2.0] - 2026-09-04
9
+
10
+ The kernel is at revision `walnutpie-warmup-telemetry-tau0.6-m1-r2-e1-d3-v10`
11
+ (unchanged since 0.1.0-beta.2: every pinned fingerprint and oracle still
12
+ holds); the paper adaptation mode advances to
13
+ `walnutpie-paper-adaptation-kquantile-gamma-v4`. Evidence for every claim is a
14
+ checksummed study under `STUDIES/` (study codes in brackets). Summary in
15
+ `wiki/release-0.2.0.md`.
16
+
17
+ ### Upgrading from 0.1.0-beta
18
+
19
+ - The `walnutpie` facade is still exported unchanged; every 0.1 call site
20
+ compiles. New code should use `owalnuts::sampler`, whose `run` paths are
21
+ thin wrappers over the same entry points and produce bit-identical draws.
22
+ - Research-only items (`OuterOrbitSelection`,
23
+ `ResearchTargetEvaluationLimit`, `ResearchRestartReferenceMultiplier`,
24
+ `DualAveragingAcceptance::AcceptedTrajectory`,
25
+ `TargetEvaluationLimitProvenance::ExplicitResearchOptIn`, the
26
+ `direct_original_q` family, the projected/pooled arrowhead warmup) now need
27
+ `features = ["research"]`.
28
+ - `sampler::Tuning::default()` is **not** `walnutpie::KernelTuning::default()`,
29
+ and `RunConfig` keeps the frozen legacy defaults: the sampler defaults are
30
+ max depth 10, macro step `h = 0.5` and `delta = 1`, with eight refinement
31
+ levels; `KernelTuning::default()` remains the frozen replay tuning of
32
+ `ALGORITHM_REVISION` (depth 3). Runs configured through `RunConfig` are
33
+ unaffected; runs configured through `Sampler` without an explicit
34
+ `.tuning(..)` use the new defaults.
35
+ - `PaperAdaptationConfig::default()` changed behaviour (v4, see Changed). The
36
+ v3 behaviour is one builder call away.
37
+ - `Sampler` caches the initial evaluation by default (see Changed): draws
38
+ are bit-identical but every transition makes one target call fewer, so
39
+ target-call totals and ESS-per-call figures are not comparable with 0.1
40
+ runs; `Sampler::cache_initial_evaluation(false)` restores the old
41
+ accounting, and `RunConfig` keeps the cache off.
42
+
43
+ ### Added
44
+
45
+ - **Warmup-time chain rescue** (`STUDIES/chain_rescue_v1`,
46
+ [WP33-CHAIN-RESCUE-V1]). `walnutpie::WarmupConfig::with_chain_rescue(ChainRescueConfig)`
47
+ makes the multi-chain diagonal driver (`sample_chains_with_control` and
48
+ its wrappers; the `sampler` identity and diagonal paths) advance the
49
+ chains window by window and meet at the end of every slow metric window.
50
+ `ChainRescueConfig::restart_from_best()` scores every chain on the window
51
+ just completed (post-boundary step; median and IQR of the selected
52
+ states' log density) and re-seeds an outlier — step below 0.1x the
53
+ chains' median, or median log density more than three within-chain IQRs
54
+ below the chains' median — from the largest-step non-outlier chain's
55
+ window: one of its positions (drawn with the outlier's own RNG stream),
56
+ its metric, step and dual-averaging state. `pool_at_boundaries()` merges
57
+ the chains' window variances exactly and installs the pooled metric and
58
+ the median step everywhere. Both act only on discarded transitions;
59
+ retained draws come from the unchanged per-chain kernel, and a boundary
60
+ with no outlier changes nothing. Every decision is a `ChainRescueUpdate`
61
+ (`window_index`, `transition`, `chain`, scores, `ChainRescueOutcome::{Kept,
62
+ Skipped, Restarted { source, criterion, source_position, step_after },
63
+ Pooled }`) in `RunTelemetry::chain_rescues`. Internally `run_chain` is
64
+ now `ChainRun::start / advance / finish`; the plain path runs the three
65
+ back to back on one thread and is bit-identical (every fingerprint and
66
+ test unchanged). Single-chain runs ignore the option; the dense and
67
+ structured-refresh facades reject it. Tests: `tests/chain_rescue.rs`
68
+ (determinism across seeds and thread counts, the density rule fires on a
69
+ chain started in a trap and the rescued chain samples the main mode,
70
+ never fires on a Gaussian and the draws are the plain run's, every
71
+ record lies in warmup, skipped boundaries and one chain are the plain
72
+ run, dense rejects).
73
+
74
+ - **Orbit-position diagnostics** (`STUDIES/kernel_gap_v1`,
75
+ [WP30-KERNEL-GAP-V1]). `walnutpie::TransitionDiagnostics` gains
76
+ `orbit_states` (states in the final orbit), `selected_index` and
77
+ `initial_index` (positions of the selected and the initial state within
78
+ it, from the backward end); `WorkTotals` gains
79
+ `accepted_forward_micro_steps` (target calls attached to a built leaf)
80
+ and `refinement_level_built` (built leaves by refinement level). Counters
81
+ only: draws, call counts and fingerprints are unchanged. The kernel's
82
+ `Span` carries `states` and `selected_offset` for them. The reference
83
+ NUTS of `examples/kernel_efficiency.rs` moved to
84
+ `examples/support/reference_nuts.rs` (any `Target`, the same orbit
85
+ statistics); `examples/funnel_kernel_options.rs` takes `--seed` and
86
+ `--sampler-defaults`. The study decomposes the per-gradient gap to NUTS
87
+ on six posteriordb models at CmdStan's adapted step, metric and starts:
88
+ gradients per leaf 1.01x and no selection difference; leaves per orbit
89
+ 0.60x under the endpoint U-turn statistic (41-69 % recursive U-turns);
90
+ `UTurnRule::MomentumSum` restores 0.97x leaves and 0.90x ESS per gradient
91
+ (default 0.77x), with the residual refinement's reverse-coarser stops.
92
+ No new option; the recommendation is to re-decide `MomentumSum` as the
93
+ default jointly with `DiagonalMetricRegularization::Stan`.
94
+ - **Per-chain R-hat attribution** (`STUDIES/step_collapse_v1`,
95
+ [WP27-STEP-COLLAPSE-V1]). `diagnostics::Summary` carries
96
+ `chain_disagreement: Option<ChainDisagreement>`: when the maximum rank
97
+ R-hat over parameters exceeds `diagnostics::RHAT_DISAGREEMENT_THRESHOLD`
98
+ (1.01) and there are at least three chains, the maximum R-hat is
99
+ recomputed with each chain left out and the chains whose removal alone
100
+ brings it below the threshold are named (a chain in a second mode, a
101
+ chain that never left its start); an empty list says no single chain
102
+ explains the failure. `Summary`'s `Display` prints the line.
103
+ - **Two opt-in warmup step floors**, off by default, measured and not
104
+ recommended (`STUDIES/step_collapse_v1`): `WarmupConfig::with_step_floor_relative_to_search`
105
+ (the adapted step is at least a fraction of the latest initial-step
106
+ search result; requires a search) and `WarmupConfig::with_max_window_shrink`
107
+ (the adapted step never falls below the step its dual-averaging stream
108
+ started from divided by a factor). Neither moves a collapsed step, the
109
+ search floor loses 30 % on the controls, and the shrink bound pins
110
+ `arma11` chains that need to slide through thirty orders of magnitude.
111
+ - **The frozen-chain escape rule** (`STUDIES/freeze_mode_v1`,
112
+ [WP24-FREEZE-MODE-V1]). `walnutpie::ExhaustionRule::AcceptUnlessDivergent`
113
+ keeps the finest attempt of a leaf that failed `delta` at every refinement
114
+ level unless it is divergent in Stan's sense, `H_end - H_0 >
115
+ divergence_threshold` with `H_0` the transition's initial Hamiltonian:
116
+ one-sided and relative to the transition, so an energy drop of any size is
117
+ accepted and, once the trajectory sits below `H_0`, a leaf's own error no
118
+ longer ends the orbit. Rises below the rounding noise of `H_0`
119
+ (`|H_0| * 2^-40`, `walnutpie::HAMILTONIAN_NOISE_RELATIVE`) count as zero,
120
+ and such a leaf feeds Stan's `min(1, exp(H_0 - H_end))` to the step
121
+ statistic. `WarmupConfig::with_warmup_exhaustion_rule` applies a rule to
122
+ the discarded transitions only. This is what lets a chain slide out of a
123
+ start where the leapfrog is unstable at every step size: the `arma11`
124
+ starts of `posteriordb_bench_v2` (log density -4.5e19 to -1.8e115) froze
125
+ every oWALNUTS arm because WALNUTS' two-sided `|H_end - H_start| <= delta`
126
+ turns every downhill step into an exhaustion and the coarse-endpoint
127
+ statistic then drives dual averaging into a floating-point no-op
128
+ equilibrium (`q + h v == q`); CmdStan escapes the same starts through its
129
+ one-sided test. The one-sided rule is *not* reversible beyond the
130
+ threshold (the funnel tail mass at the sampler defaults drops from 0.024
131
+ to 0.014 when it is applied to retained draws — as does the existing
132
+ two-sided `AcceptBelowDivergenceThreshold`, 0.013), hence the warmup-only
133
+ option.
134
+ - `WarmupConfig::with_minimum_step`: a floor on the adapted step (a
135
+ negative control of the study; a chain whose every leaf fails at every
136
+ step size is not helped by it).
137
+ - `walnutpie::TransitionDiagnostics::{step_size, position_changed,
138
+ acceptance_statistic}`: the macro step the transition ran with, whether
139
+ the selected position differs from the initial one, and the statistic
140
+ fed to dual averaging.
141
+ - `tests/freeze_mode.rs`: the pin and the escape on a synthetic Gaussian
142
+ with an exponential wall, without BridgeStan.
143
+
144
+ - **`owalnuts::sampler`, the 0.2 public API.** One builder, `Sampler`
145
+ (`warmup`, `draws`, `chains`, `seed`, `threads`, `metric`, `adaptation`,
146
+ `tuning`, `limits`, `run`, `run_with_init`, `run_from_random_starts`), one
147
+ result, `Posterior` (chains, flat and per-draw access, telemetry, metadata,
148
+ refresh records), and five small option types: `Metric` (`Identity`,
149
+ `Diagonal`, `Dense`, `Structured`, `StructuredRefresh`), `Adaptation`
150
+ (`None`, `DualAveraging` — the default — `Paper`, and
151
+ `Custom(WarmupConfig)` for the opt-in Stan-parity controls below), `Tuning`,
152
+ `Limits` (target-evaluation budget, worst-case admission, deadline, timeout,
153
+ cancellation, depth-stop limit) and `Init`. Every `run` path is a thin
154
+ wrapper over one `walnutpie` entry point and produces bit-identical draws to
155
+ calling it directly (`tests/sampler_api.rs`); kernel behaviour is unchanged.
156
+ The README quick start and the `gaussian` and `funnel_paper_adaptation`
157
+ examples use it.
158
+ - **Uniform starts with retries.** `sampler::Init` (`Given`, `Uniform {
159
+ radius, max_attempts }`, default `Init::uniform()` = Stan's uniform(-2, 2)
160
+ with 100 attempts), `uniform_starts`, `Sampler::run_with_init` and
161
+ `Sampler::run_from_random_starts`: a start is redrawn until the log density
162
+ and gradient are finite, deterministically given the seed (start RNG
163
+ `splitmix64(seed ^ INIT_SEED_TAG)`, independent of the chain seeds).
164
+ `STUDIES/posteriordb_bench_v1` aborted two `lotka_volterra` seeds on an
165
+ unevaluable single draw; with this rule they proceed as CmdStan's do.
166
+ [WP22-POSTERIORDB-BENCH-V1 follow-up]
167
+ - **`research` Cargo feature (off by default).** The research-only items
168
+ listed under Upgrading are exported from `walnutpie` only with the feature.
169
+ They are still compiled (`src/walnutpie/research.rs`), so no kernel path
170
+ changed; the `STUDIES/` crates and the Python integration enable the
171
+ feature.
172
+ - **Research-only `NonfinitePositionPolicy`.** `KernelTuning::
173
+ with_nonfinite_position` / `sampler::Tuning::nonfinite_position` select
174
+ what a transition does when the integrator hands the target a nonfinite
175
+ position. The default `Abort` is the frozen `v10` behaviour (the run ends
176
+ with `ErrorKind::Numerical`); the opt-in `RejectLeaf` treats the point as a
177
+ zero-density leaf with a zero gradient, like a recoverable target failure,
178
+ and counts it in `WorkTotals::nonfinite_position_rejections`. Runs in which
179
+ the event never occurs are bit-identical under either policy; fingerprints
180
+ unchanged. Motivated by `STUDIES/sspd_target_fatal_diag_v1`; qualified as
181
+ an opt-in only by `STUDIES/nonfinite_position_policy_v1` (WP38).
182
+ - **Diagnostics and CmdStan export.** `owalnuts::diagnostics` computes
183
+ rank-normalised folded split R-hat, bulk/tail/quantile/mean ESS, MCSE of
184
+ the mean, and type-7 quantiles per parameter from `&[&[f64]]` chain views
185
+ (Vehtari et al. 2021; every estimator matches `az.rhat`, `az.ess` and
186
+ `az.mcse` to 1e-6 relative on the committed
187
+ `tests/data/arviz_fixture.json`), and `Summary::from_output` builds a
188
+ Stan-style table for a `MultiChainOutput` with per-chain and pooled
189
+ `SamplerHealth` (divergences, invalid-evaluation, depth-cap and
190
+ refinement-exhaustion stops, mean tree depth, target calls, step size);
191
+ `Summary` implements `Display` as an aligned table.
192
+ `owalnuts::export::CmdStanCsv` writes one CmdStan-format CSV per chain
193
+ (`lp__` recomputed from the target when supplied, `stepsize__`,
194
+ `treedepth__`, `n_leapfrog__` as fused target calls, `divergent__`,
195
+ `energy__` as the transition's initial Hamiltonian, then the draws);
196
+ `arviz.from_cmdstan` loads the files and its `az.summary` agrees with the
197
+ Rust `Summary` (`tests/export_cmdstan.rs`, opt-in via
198
+ `OWALNUTS_ARVIZ_PYTHON`). `accept_stat__` is not emitted because the kernel
199
+ captures acceptance only during warmup. No new dependencies.
200
+ - **Opt-in Stan-parity warmup controls** on `walnutpie::WarmupConfig`:
201
+ `DualAveragingAcceptance::MeanTrajectoryAcceptance` (Stan's
202
+ `accept_stat__`), `InitialStepSearchConfig::stan()` (Stan's
203
+ `init_stepsize`, at the start and after every metric update),
204
+ `DiagonalMetricRegularization::{TowardUnit, Stan}`,
205
+ `with_stan_restart_reference` (`mu = ln(10 h)` on restart),
206
+ `with_initial_phase_max_error` (a different `delta` for the initial fast
207
+ phase), and the preset `WarmupConfig::stan_style(target)`;
208
+ `sampler::Adaptation::Custom(WarmupConfig)` passes any of them through the
209
+ builder. Evidence in `STUDIES/adaptation_parity_v1`: alone, none of the
210
+ four Stan warmup differences helps (Stan's metric prior freezes chains
211
+ started in a tail under `delta = 1`); with the initial-phase `delta` the
212
+ full preset at depth 10 is 2.0x the default's ESS per gradient (0.68x
213
+ CmdStan) but loses 12-16 % on `kidiq`, `mesquite`, `garch11` and fails
214
+ R-hat on `kidiq`, `earnings`, so it stays opt-in.
215
+ - **Opt-in kernel rule variants** (`STUDIES/kernel_efficiency_v1`).
216
+ `walnutpie::KernelOptions` (`KernelTuning::with_options`,
217
+ `sampler::Tuning::kernel_options`) selects the no-U-turn predicate
218
+ (`UTurnRule::{Endpoints, EndpointsWithCross, MomentumSum}`; `MomentumSum`
219
+ is Stan's generalised criterion on the sum of the leaf momenta with the
220
+ 2.21+ cross checks) and the treatment of a leaf that fails `delta` at
221
+ every refinement level (`ExhaustionRule::{Stop,
222
+ AcceptBelowDivergenceThreshold}`; the latter is Stan's rule, subject to
223
+ the usual reverse coarsening check so the leaf stays reversible).
224
+ `RunConfig::with_cached_initial_evaluation` reuses the previous
225
+ transition's selected log density and gradient instead of re-evaluating
226
+ the current position at the start of every transition. `KernelOptions::
227
+ default()` and the cache off reproduce the frozen fingerprints; on the
228
+ 100-D Gaussian `MomentumSum` with the cache takes the kernel from 0.81x to
229
+ 1.09x reference NUTS ESS per gradient, is neutral within seed noise on the
230
+ correlated Gaussian and Eight Schools, and preserves the funnel tail mass
231
+ (`examples/funnel_kernel_options.rs`). The preregistered posteriordb
232
+ decision (`STUDIES/uturn_default_v1`, [WP26-UTURN-DEFAULT-V1]) kept
233
+ `Endpoints` as the default: `MomentumSum` is 1.06x geomean over the 17
234
+ models (1.18–2.14x on six, 0.78–0.93x on seven), passes one cell fewer,
235
+ and the funnel at the sampler defaults is biased under every rule.
236
+ - **Additive, off-by-default Appendix C guards** on `PaperAdaptationConfig`:
237
+ `with_min_max_error`, `with_first_update_after`,
238
+ `with_metric_update_required`, `with_unhealthy_orbits_excluded`,
239
+ `with_trim_fraction`, `with_exhausted_transitions_as_zero` (a default since
240
+ v4, see Changed), plus `PaperAdaptationOutcome::Deferred`. Measured in
241
+ `STUDIES/paper_adaptation_robust_v1`; kernel fingerprints unchanged.
242
+ - **FFI and autodiff backend support.** `RawTarget` wraps a C-ABI fused
243
+ log-density/gradient callback (`RawTargetFn`) so compiled gradients —
244
+ numba/Cython `cfunc`s, BridgeStan-style entry points — run from parallel
245
+ chains with no interpreter lock; `-inf` returns follow the v10 recoverable
246
+ zero-density path and any other nonfinite output is fatal. References,
247
+ boxes, and `Arc`s of targets are now targets (`&dyn Target` works
248
+ everywhere). Fatal target error messages are carried into `Error` and shown
249
+ by its `Display`. `Target::parameter_names` (default `None`) labels
250
+ unconstrained coordinates for diagnostics export. Motivated by the autodiff
251
+ track's measurement that GIL-free callback transport, not sampler
252
+ efficiency, was the remaining gap to nutpie on PyMC models.
253
+ [WP15a-AUTODIFF-BRIDGESTAN-ENZYME-V1, WP15B-PYTHON-TARGETS-V1]
254
+ - **`owalnuts-autodiff` (`integrations/autodiff`, unpublished).** Write a
255
+ log density once as `fn log_density<S: Scalar>(&self, q: &[S])`, evaluate
256
+ it with `f64` or with `Var` on a reusable thread-local arena tape, and get
257
+ an `AutodiffTarget<M>` implementing `walnutpie::Target`. Fused primitives
258
+ (`normal_lpdf`/`lupdf` with broadcasting, Student-t, Cauchy, lognormal,
259
+ exponential, gamma, half-normal, `bernoulli_logit`, `poisson_log`, `dot`,
260
+ `sum`, `log_sum_exp`, `cumsum`, `softplus`, and the exp/logistic/interval/
261
+ ordered constraints with log-Jacobians) with hand-gradient and
262
+ finite-difference oracles; numbers in `integrations/AUTODIFF-RESEARCH.md`.
263
+ - **`owalnuts-bridgestan` (`integrations/bridgestan`, unpublished):**
264
+ `ReplicatedStanTarget` (one library copy per thread, `try_lock` dispatch)
265
+ and a per-library-file serialising mutex for `StanTarget`, after the
266
+ measurement that `STAN_THREADS` on mingw-w64 costs 9-16x per gradient
267
+ (emulated TLS); the non-threaded build matches CmdStan's per-gradient
268
+ cost (`arK` wall 10.5 s -> 1.5 s against CmdStan's 1.0 s, trajectories
269
+ bit-identical). [WP22-POSTERIORDB-BENCH-V1 follow-up]
270
+ - **Boundary-refreshed structured metrics.** `sample_structured_refresh`,
271
+ `sample_chains_structured_refresh`, and
272
+ `preflight_chains_structured_refresh` run the fixed kernel directly in
273
+ original coordinates through a `StructuredBlockMass` operator and rebuild it
274
+ with a caller-supplied `StructuredMetricRefresh` at every completed slow
275
+ warmup-window boundary (per-chain Welford `WindowSummary`; typed
276
+ `StructuredRefreshUpdate` telemetry; optional boundary step re-search;
277
+ `StructuredRefreshRestartPolicy` for dual averaging; failures keep the
278
+ previous metric installed; the metric freezes before the first retained
279
+ transition). Execution identity `STRUCTURED_REFRESH_REVISION`
280
+ (`walnutpie-structured-metric-refresh-v1`); an identity refresh is
281
+ bit-identical to the fixed direct driver, and `ALGORITHM_REVISION` is
282
+ unchanged. Motivated by the T=1000 state-space result that the posterior-
283
+ precision path block depends on global parameters best estimated during
284
+ warmup. [WP4B-REAL-TARGET-PATH-METRIC-V1, WP12-SSPD11-CONFIRMATION-V1]
285
+ - **Python package `owalnuts` 0.2.0** (`integrations/python`, unpublished):
286
+ every run builds an `owalnuts::sampler::Sampler`, so the package inherits
287
+ the sampler defaults (see Fixed); `owalnuts.DEFAULTS` reports them
288
+ read-only from the Rust constants; `init="uniform"` start rule
289
+ (`Init::uniform`), `Tuning(u_turn_rule=, exhaustion_rule=)` and
290
+ `Adaptation(metric_regularization=)` overrides, `SampleResult.summary()`
291
+ backed by `owalnuts::diagnostics`, plus the `from_cfunc` /
292
+ `from_pymc(gil_free=True)` GIL-free transport and the structured-metric
293
+ refresh callback. See its README.
294
+ - **Python package on PyPI.** `pip install owalnuts` (`owalnuts[stan]`,
295
+ `[jax]`, `[torch]`, `[pymc]`, `[numba]`, `[arviz]`): abi3 wheels
296
+ (CPython 3.10+, one per platform) for Linux x86_64/aarch64 (manylinux),
297
+ macOS x86_64/arm64 and Windows x86_64 plus a self-contained sdist (maturin
298
+ vendors the root crate and `integrations/bridgestan` into it), built and
299
+ tested against a fresh interpreter by `.github/workflows/wheels.yml` and
300
+ published from `v*` tags through PyPI trusted publishing.
301
+ - **Stan models from Python.** `owalnuts.from_stan(stan_file, data, seed=,
302
+ make_args=)` compiles a Stan program with the `bridgestan` package on Linux
303
+ and macOS (no `STAN_THREADS` by default) and
304
+ returns a `StanTarget` that `owalnuts.sample` runs through the Rust
305
+ `owalnuts_bridgestan::ReplicatedStanTarget` — snapshotted library replicas,
306
+ GIL-free — with the model's
307
+ unconstrained parameter names on the result (`bs_param_unc_names`, new in
308
+ the bridgestan crate) and `StanTarget.constrain(result)` for the
309
+ constrained draws (`bs_param_constrain`). Cargo feature `stan` of the
310
+ extension (default on); the bridgestan crate's tape-backend `bench` binary
311
+ moved behind its own `bench` feature. Windows 0.2 disables `from_stan` and
312
+ direct Python model operations; use the Rust owned-worker API.
313
+ - `examples/kernel_bench.rs` (kernel hot-path microbenchmark) and
314
+ `tests/kernel_fingerprint.rs` (bit-exact run fingerprints in both build
315
+ profiles).
316
+
317
+ ### Changed
318
+
319
+ - **POST-STUDY DEFAULT CHANGE (WP36): chain rescue is disabled by default.**
320
+ `Adaptation::DualAveraging` and `Adaptation::Paper` no longer install a
321
+ rescue on identity or diagonal multi-chain runs, and
322
+ `sampler::DEFAULT_CHAIN_RESCUE` is now the truthful
323
+ `Option<ChainRescueConfig> = None` (`STUDIES/chain_rescue_v2`,
324
+ [WP36-CHAIN-RESCUE-V2]). WP36 launched all 288 frozen one-shot cells; 281
325
+ were process-valid, while six Windows heap-corruption exits and one
326
+ post-result timeout left six paired triplets invalid. The registered
327
+ completeness gate therefore failed. Two-hit reduced nuisance unique-chain
328
+ actions from 35 to 14, but `two_hit` failed its conjunctive gates, including
329
+ the registered minimum sample size, efficacy, funnel, origin-safety and
330
+ efficiency requirements. That failure alone did not select `no_rescue`; it
331
+ advanced the mechanical rule to the `current` fallback check. `current`
332
+ separately had registered red lines in four origin-overwrite cells (five
333
+ events) plus unknown run history for HMM/92104, so the fallback selected
334
+ `no_rescue`. The classifier found pathological/frozen ARMA and
335
+ Lotka-Volterra origins and zero HMM origins, so WP36 does not establish
336
+ genuine posterior-mode destruction. Immediate
337
+ `restart_from_best` retains WP33's strong bad-start efficacy and remains
338
+ fully opt-in, together with observe-only, two-hit and pooling, through
339
+ `Adaptation::Custom(WarmupConfig::with_chain_rescue(...))`. A restart that
340
+ acts copies state between chains and therefore invalidates the
341
+ independent-start interpretation of ordinary R-hat. Historical WP35 replay
342
+ must use study revision `8d3a7b5` and its recorded source under test
343
+ `aa4510f`: running `STUDIES/posteriordb_bench_v6` at current HEAD would use
344
+ the intentional no-rescue `Adaptation::default()` and would not reproduce
345
+ WP35. Default sampler output is bit-identical to an otherwise identical
346
+ explicit custom no-rescue warmup, and its rescue telemetry is empty. This is
347
+ the post-study default commit, not an algorithm revision: the retained kernel,
348
+ `walnutpie::WarmupConfig` / `RunConfig` defaults, walnutpie facade outputs,
349
+ and all core fingerprints are unchanged.
350
+
351
+ - **HISTORICAL TEMPORARY DEFAULT CHANGE (WP33, by the preregistered rule): the sampler's own
352
+ adaptation modes apply `sampler::DEFAULT_CHAIN_RESCUE =
353
+ ChainRescueConfig::restart_from_best()` on the identity and diagonal
354
+ metrics with at least two chains** (`STUDIES/chain_rescue_v1`,
355
+ [WP33-CHAIN-RESCUE-V1]). The study preregistered the rule — a candidate
356
+ flips if it passes >= 3 more of 27 cells (8 posteriordb models x 3 fresh
357
+ seeds + 3 funnel cells) than the plain driver, no model below 0.9x
358
+ ESS/gradient, no new reference disagreement |z| > 3.5, funnel |z| <= 2 on
359
+ every seed — before the implementation existed, and restart-from-best
360
+ met every part: 25 vs 21 cells (`lotka_volterra` 0/3 -> 3/3, the frozen
361
+ chain caught at the first boundary, 25 min -> 5 min on one seed; `arma11`
362
+ 2/3 -> 3/3, the crawl cell R-hat 1.60 -> 1.003), per-model ESS/gradient
363
+ 1.00–289x (geomean 2.72), `mesquite` and `nes2000` byte-identical,
364
+ funnel z +0.94 / −0.77 / −1.02, max |z| unchanged within noise. Pooling
365
+ (`pool_at_boundaries`) gained two cells and fell to 0.42x on
366
+ `lotka_volterra`; it stays opt-in. Draws of every multi-chain `Sampler`
367
+ run on an identity or diagonal metric with `Adaptation::DualAveraging`
368
+ or `Adaptation::Paper` can change (they are the plain draws whenever no
369
+ chain is an outlier at any boundary); `Adaptation::Custom` is used as
370
+ given (the opt-out), dense and structured metrics, single-chain runs,
371
+ `walnutpie::WarmupConfig::default()`, `RunConfig`, `ALGORITHM_REVISION`
372
+ and the kernel fingerprints are unchanged. Read
373
+ `RunTelemetry::chain_rescues` before trusting R-hat on a multimodal
374
+ target: a `LogDensity` rescue merges a chain into the others and hides
375
+ the mode it had found. Three of six predictions failed and are reported
376
+ (the density rule also fires on chains merely late at the first
377
+ boundary; the funnel 88101 R-hat is not a one-bad-chain failure; the
378
+ rule was predicted not to be met). `tests/sampler_api.rs` mirrors the
379
+ new default.
380
+
381
+ - **DEFAULT CHANGE (post-hoc after WP31): `sampler::Tuning::default()`
382
+ U-turn rule `Endpoints` -> `MomentumSum`, and the sampler's own
383
+ adaptation modes regularise the diagonal metric with Stan's prior**
384
+ (`STUDIES/joint_default_v1`, [WP31-JOINT-DEFAULT-V1]; validated by
385
+ `STUDIES/posteriordb_bench_v5`, [WP32-POSTERIORDB-BENCH-V5]). New
386
+ constants `sampler::DEFAULT_U_TURN_RULE = UTurnRule::MomentumSum` and
387
+ `sampler::DEFAULT_METRIC_REGULARIZATION = DiagonalMetricRegularization::Stan`;
388
+ `Adaptation::DualAveraging` and `Adaptation::Paper` apply the latter to
389
+ the `WarmupConfig` they build (as they apply `DEFAULT_WARMUP_EXHAUSTION`);
390
+ `Adaptation::Custom` is used as given. This is a **post-hoc decision**:
391
+ WP31 preregistered a flip rule (geomean >= 1.15x, no model < 0.85x, gates
392
+ >= the default's, funnel |z| <= 2 at both tunings, Eight Schools >= 0.9x)
393
+ and the pair met four of five — 1.51x geomean over the 17 posteriordb
394
+ models, 41 vs 35 cells, funnel exact with zero divergences, Eight Schools
395
+ 1.29x — but failed the per-model floor on the two cells no option passes
396
+ (`hmm_drive_0`'s arm-dependent second-mode draw at 0.005x, the centered
397
+ eight schools at 0.79x and 0/3 everywhere), so the study did not flip.
398
+ The flip was decided afterwards on that evidence and validated on fresh
399
+ seeds 87101–87103 against CmdStan 2.39.0 and nutpie 0.16.8 with
400
+ preregistered predictions (WP32; numbers in `wiki/release-0.2.0.md`).
401
+ Draws of every `Sampler` run that adapts a diagonal metric or uses
402
+ `Tuning::default()` change; `walnutpie::KernelOptions::default()`,
403
+ `walnutpie::WarmupConfig::default()`, `RunConfig`, `ALGORITHM_REVISION`
404
+ and the kernel fingerprints are unchanged, and the old behaviour is
405
+ `Tuning::new().kernel_options(KernelOptions::default())` with
406
+ `Adaptation::Custom(WarmupConfig::new(0.8).with_warmup_exhaustion_rule(DEFAULT_WARMUP_EXHAUSTION).with_metric_regularization(DiagonalMetricRegularization::TowardUnit))`.
407
+ `tests/sampler_api.rs` mirrors the new defaults, and the Python package
408
+ inherits them through `Sampler` (see Fixed).
409
+ - **`sampler::Tuning::default()` refinement levels 4 -> 8**
410
+ (`STUDIES/funnel_defaults_v1`, [WP28-FUNNEL-DEFAULTS-V1]). At four
411
+ levels the sampler defaults (adapted diagonal metric, dual averaging,
412
+ `h0 = 0.5`) put half the exact mass below `omega = -5` on Neal's funnel
413
+ (0.0203 / 0.0242 / 0.0625 on three fresh seeds against 0.0478, z -3.5 /
414
+ -3.8 / +0.3, ~1,000 retained refinement exhaustions per cell): the
415
+ adapted step cannot enter the neck with micro-steps no finer than
416
+ `h / 16`. At eight levels (`h / 256`) the tail mass is 0.0412 / 0.0346 /
417
+ 0.0897 (|z| <= 1.43), exhaustions fall 26x and divergences 7x, at 1.08x
418
+ the funnel's target calls, 1.05x the ESS per call on the noncentered
419
+ Eight Schools and 1.00x on a 100-D Gaussian (two of three seeds
420
+ call-for-call identical: the cap never engages there). The preregistered
421
+ grid also rejected `delta` 0.5 / 0.25 alone (more exhaustions, worse
422
+ bias), `Adaptation::Paper` from `h0 = 0.5` (one seed at z -2.7; 0.4x on
423
+ the Gaussian) and the `stan_style` preset (biased; one cell errored on a
424
+ nonfinite density). Eight levels remove the bias, not the funnel's poor
425
+ mixing under dual averaging (one chain per seed at `h ~ 0.01`, `omega`
426
+ R-hat 1.01-1.04); `max_error(0.5)` with eight levels mixes better there
427
+ at 0.79x on the Gaussian and stays opt-in. `tests/sampler_api.rs` and the
428
+ Python package default follow; `walnutpie::KernelTuning::default()`,
429
+ `RunConfig`, `ALGORITHM_REVISION` and the kernel fingerprints are
430
+ unchanged.
431
+
432
+ - **`sampler` default warmup exhaustion rule.** `Adaptation::DualAveraging`
433
+ and `Adaptation::Paper` now apply `sampler::DEFAULT_WARMUP_EXHAUSTION`
434
+ (`ExhaustionRule::AcceptUnlessDivergent`, below) to the discarded
435
+ transitions; retained transitions keep `Tuning::kernel_options` (the
436
+ frozen two-sided rule), so no retained draw of the frozen kernel changes
437
+ and warmup differs only where a leaf exhausts at every level.
438
+ `Adaptation::Custom` configurations are used as given. On the
439
+ `posteriordb_bench_v2` starts this turns `arma11` from 7 frozen chains of
440
+ 12 into 0 (min bulk ESS 1,290-1,460 against 4-7) and passes two of three
441
+ `lotka_volterra` seeds against one, with the funnel tail mass and the
442
+ centered Eight Schools unchanged or better (`STUDIES/freeze_mode_v1`).
443
+ `tests/sampler_api.rs` mirrors the new default; `ALGORITHM_REVISION` and
444
+ the kernel fingerprints are unchanged.
445
+
446
+ - **`sampler::Limits` admits the exact worst case by default.** The sampler's
447
+ own defaults (depth 10, eight refinement levels) exceed the conservative
448
+ `walnutpie` admission ceiling for ordinary 4 x 1,000/2,000 runs, which made
449
+ `Sampler::run` fail with a resource-limit error. The worst case is an exact
450
+ bound the run cannot exceed, so admitting against it costs nothing; draws are
451
+ unchanged. `Limits::admit_conservative` restores the 0.1.x check.
452
+ - **`sampler::Tuning::default()` max depth 8 -> 10** (Stan's default). Chosen
453
+ by the preregistered ablation `STUDIES/adaptation_parity_v1` (nine
454
+ posteriordb models, two seeds): 1.45x geometric-mean minimum bulk ESS per
455
+ gradient over the 0.1 defaults, 17/18 gate passes versus 12/18, no model
456
+ worse beyond seed noise; the correlated regressions `diamonds`, `earnings`
457
+ and `sblrc` capped 55-85 % of transitions at depth 8 and failed every gate.
458
+ Geomean versus CmdStan on the nine models: 0.34x -> 0.49x. Only the
459
+ `sampler` default changes; `walnutpie::KernelTuning::default()` and
460
+ `WarmupConfig::default()` are the frozen `v10` legacy, `ALGORITHM_REVISION`
461
+ is unchanged and the kernel fingerprints still hold.
462
+ - **`sampler::Sampler` caches the initial evaluation by default.** Draws are
463
+ bit-identical to the uncached run; one target call per transition is saved
464
+ (11 % of the gradients on Eight Schools and the 100-D Gaussian at 8-9
465
+ leaves per orbit, `STUDIES/kernel_efficiency_v1`). `walnutpie::RunConfig`
466
+ keeps the cache off so the frozen target-call fingerprints hold;
467
+ `Sampler::cache_initial_evaluation(false)` restores the old accounting.
468
+ - **`PaperAdaptationConfig::default()` is `walnutpie-paper-adaptation-kquantile-gamma-v4`.**
469
+ `with_exhausted_transitions_as_zero(true)` and
470
+ `with_step_relative_bound(DEFAULT_PAPER_STEP_RELATIVE_BOUND = 1e6)` are
471
+ now the defaults. `STUDIES/paper_adaptation_robust_v1` traced the
472
+ posteriordb freezes to leaf-less transitions producing no `h` statistic
473
+ and then to the `1e3` step band, not to the `delta` rule; with both
474
+ guards the default is robust on all 14 freeze-model cells and
475
+ 0.90-1.35x dual averaging's min bulk ESS per gradient (geomean 1.04).
476
+ The `v3` behaviour is
477
+ `.with_exhausted_transitions_as_zero(false).with_step_relative_bound(PAPER_STEP_RELATIVE_BOUND)`.
478
+ Acceptance-driven warmup, `ALGORITHM_REVISION` and the kernel
479
+ fingerprints are unchanged.
480
+ - **Allocation-free kernel hot path, bit-identical.** Micro-steps write the
481
+ gradient into the state's own buffer (`FusedEval`), a per-transition
482
+ workspace replaces the clones in refinement and reverse coarsening, span
483
+ endpoints share `Rc` states, leaf states live in a per-thread ring, the
484
+ endpoint joint log density is reused for the reverse check, and the
485
+ per-call kinetic energy is skipped when no proposal observer is attached.
486
+ Kernel overhead per fused target call (single thread, best of 3,
487
+ `examples/kernel_bench.rs`): funnel 480 -> 255 ns, Gaussian-100D 2002 ->
488
+ 936 ns, Eight Schools 889 -> 510 ns; allocations per call 8.5 -> 0.19,
489
+ 14 -> 0.56 and 14.4 -> 1.27. Every fingerprint, oracle and facade test is
490
+ unchanged in both build profiles.
491
+ - Crate description, README and module docs point first-time readers at
492
+ `owalnuts::sampler`; the `walnutpie` docs describe it as the complete
493
+ facade underneath.
494
+
495
+ ### Fixed
496
+
497
+ - **Windows BridgeStan native lifetime is isolated on one owned OS thread.**
498
+ Every library/symbol, construct, metadata/name, gradient/error-free, and
499
+ model-destruct call now runs on that owner; bounded-channel callers never
500
+ enter model code, and drop joins through native TLS teardown. Windows
501
+ `ReplicatedStanTarget::requested_replicas()` preserves the caller's request
502
+ while `effective_replicas()` returns one; `threading()` reports serialized
503
+ effective execution, `compiled_threading()` preserves the DLL capability,
504
+ and `execution()` names the owned-serialized backend. Model bytes use a
505
+ verified SHA-256 cache and
506
+ module-global setup is locked; a process-global Windows native-call mutex
507
+ additionally serializes all owner threads/models through setup, evaluation,
508
+ error free, and destruction. Non-Windows replica 0 preserves the original
509
+ `$ORIGIN`/`@loader_path` path while replicas 1..n use one source snapshot.
510
+ The first resident-DLL/scoped-pool mitigation was rejected because its fixed
511
+ arm still faulted in 8/180 children. The owned-one follow-up had zero faults
512
+ in 540 children against 19/180 event-inclusive comparator faults. A fresh
513
+ fixed-only qualification of the final process-global implementation then
514
+ completed all 540 ordinary and 180 concurrent-four-target children with
515
+ zero process faults, timeouts, missing/incomplete outputs, invariant
516
+ mismatches, or correlated Event 1000 records: 0/720, with a one-sided 95%
517
+ upper bound of 0.415210%. Sampling medians remained 3.1–5.1x the
518
+ four-replica comparator. The historical root cause is not proven, and this
519
+ mitigation is qualified only on one Windows GNU host, three model binaries,
520
+ short runs, and one effective worker per target. Windows MSVC,
521
+ Linux/macOS, the package/wheel matrix, and multi-worker Windows execution
522
+ remain unqualified, so release publication stays blocked.
523
+ Python `from_stan` and direct Python `bridgestan.StanModel` call/name/
524
+ constrain paths are disabled on Windows 0.2 because they bypass this Rust
525
+ owner backend. Python `StanTarget.probe_*` metadata now names its one-load
526
+ scope; `SampleResult` alone reports the target actually used for sampling.
527
+ (`STUDIES/bridgestan_lifetime_v1`,
528
+ `STUDIES/bridgestan_owned_worker_v1`)
529
+ - **The Python package goes through `owalnuts::sampler` and inherits its
530
+ defaults.** `integrations/python` built `KernelTuning` / `WarmupConfig` on
531
+ the `walnutpie` facade directly and restated the sampler defaults, so it
532
+ silently missed the ones changed during 0.2.0 (depth 10 and eight
533
+ refinement levels were copied by hand; `DEFAULT_U_TURN_RULE`,
534
+ `DEFAULT_METRIC_REGULARIZATION`, `DEFAULT_WARMUP_EXHAUSTION`, the cached
535
+ initial evaluation and the worst-case admission were not). The PyO3
536
+ `sample` / `preflight` paths now construct `Sampler` with `Tuning`,
537
+ `Adaptation`, `Metric`, `Limits` and `Init` from the Python arguments and
538
+ send only what the caller set, `owalnuts.DEFAULTS` reads the defaults
539
+ from the Rust values, and a test checks that `owalnuts.sample` with
540
+ explicit arguments equal to `DEFAULTS` reproduces a Rust `Sampler` run bit
541
+ for bit on a 3-D Gaussian. Two consequences on the Python surface: an
542
+ explicit `max_target_evaluations` is also the admission ceiling (the
543
+ package used to admit budgeted runs against a 2^50 dummy ceiling), and a
544
+ structured-metric run at the defaults must keep its worst case under the
545
+ hard 1e9 research ceiling, as it always had to.
546
+ - **`Sampler` admits structured metrics with their worst case under the
547
+ `research` feature.** The structured facade paths have no budgeted
548
+ admission variant, so `Limits::admit_worst_case` (the default) left them
549
+ rejected at the conservative 113M ceiling whenever the sampler defaults
550
+ exceeded it. With `features = ["research"]` the sampler now raises the
551
+ `RunConfig` admission ceiling to the budget (capped at the research
552
+ maximum) when, and only when, the worst case exceeds the conservative
553
+ ceiling; runs it already admitted are configured exactly as before, and
554
+ without the feature the facade's conservative admission still applies.
555
+ `tests/sampler_api.rs` covers the admission and the rejection under
556
+ `Limits::admit_conservative`.
557
+ - **BridgeStan targets no longer abort on `NaN`/`+inf` log densities.**
558
+ `owalnuts-bridgestan` maps a `NaN`/`+inf` log density and a finite log
559
+ density with a nonfinite gradient to the recoverable zero-density path
560
+ (`map_evaluation`), as CmdStan and nutpie reject such proposals; only a
561
+ dimension mismatch stays fatal. [WP22-POSTERIORDB-BENCH-V1 follow-up]
562
+ - `sample_chains_structured` and `sample_chains_structured_with_control`
563
+ reject a target, mass, or initial position whose dimensions differ with a
564
+ configuration error instead of panicking.
565
+
566
+ ### Validation (2026-09-01 program)
567
+
568
+ - **Step collapse and the post-escape crawl (WP27-STEP-COLLAPSE-V1).** The
569
+ default step collapse on `sblrc` (h 0.003 against CmdStan's 0.10) and
570
+ `earnings` (0.003 against 0.017) is the `v10` diagonal regularisation
571
+ `(n/(n+5)) var + 5/(n+5)`, whose additive term floors every variance at
572
+ 0.0099 for `n = 500` while those coefficients have posterior variances
573
+ 1e-5 to 5e-4; dual averaging's statistic sits on target throughout and
574
+ holds `h` at the step the 30x-too-wide metric allows. Stan's statistic,
575
+ Stan's `init_stepsize` at every window, the delta ramp, the restart
576
+ reference and two step floors leave `h` unchanged;
577
+ `DiagonalMetricRegularization::Stan` alone restores CmdStan's step (`sblrc`
578
+ 9.7x ESS/gradient, `earnings` 1.9x with zero depth-cap draws, `arma11`'s
579
+ healthy chains 2.9x, controls 1.08x) but `earnings` then fails R-hat
580
+ (1.02, min bulk ESS 164–179: at CmdStan's step and metric the WALNUTS
581
+ orbit is 49 leaves against NUTS's 163, a retained-kernel U-turn matter),
582
+ so the preregistered flip rule is not met and the regularisation stays
583
+ opt-in, recommended for regressions with coefficient scales below ~0.1.
584
+ The `arma11` 79103 crawl (chain 3 at `h` 5e-8 after escaping the wall) is
585
+ a start CmdStan cannot leave in 1,000 iterations either (same log
586
+ density plateau at −3.7e5 from iteration 250, final `h` 1.7e-6, R-hat
587
+ 1.6); no warmup option changes it. Seeds 80101–80102, seven models, ten
588
+ arms, CmdStan 2.39.0 from the same starts.
589
+ - **posteriordb v3 (WP25-POSTERIORDB-BENCH-V3).** With the WP24 warmup rule, the
590
+ dual-averaging arm passes 35/51 cells (CmdStan 37, nutpie 31), zero frozen
591
+ chains, geomean 0.34x CmdStan ESS per gradient, 0.75x wall per gradient,
592
+ 1.35x nutpie ESS per second. `STUDIES/posteriordb_bench_v3`.
593
+
594
+ - posteriordb benchmark against CmdStan and nutpie
595
+ (`STUDIES/posteriordb_bench_v1`, 17 posteriors x 4 arms x 3 seeds, 204
596
+ cells): with the 0.1 defaults the dual-averaging arm was 0.32x CmdStan and
597
+ 0.25x nutpie on minimum bulk ESS per gradient over 14 models (gate passes
598
+ 26/51 versus CmdStan 34/51, nutpie 29/51), refinement engaged on 1 % of
599
+ retained leaves, and the v3 paper arm froze on nine models. This study
600
+ motivated the depth-10 default, the start retries, the BridgeStan fixes and
601
+ the Appendix C v4 defaults above.
602
+ [WP22-POSTERIORDB-BENCH-V1]
603
+ - posteriordb v2 on the 0.2.0 defaults (`STUDIES/posteriordb_bench_v2`, the
604
+ same 17 posteriors, five arms, three fresh seeds, all 255 cells present):
605
+ the dual-averaging arm passes 32/51 cells (CmdStan 35, nutpie 27; the
606
+ `stan_style` arm 32, the Appendix C v4 arm 29 versus 8 in v1) with zero
607
+ cells lost to a fatal evaluation or start failure; geomean 0.233x CmdStan
608
+ bulk ESS per gradient over 17 models (0.447x over the 15 models without a
609
+ frozen chain), 0.771x CmdStan wall per gradient and 1.108x nutpie ESS per
610
+ second. `arma11` and `lotka_volterra` chains freeze from uniform starts
611
+ where every leaf fails at every refinement level; the Appendix C v4 arm is
612
+ at parity with dual averaging (geomean 0.995), so it stays opt-in. The
613
+ breadth-throughput release gate (P1–P3, P5) is not met.
614
+ [WP23-POSTERIORDB-BENCH-V2]
615
+ - Adaptation ablation (`STUDIES/adaptation_parity_v1`, nine posteriordb
616
+ models, two seeds): depth 10 over depth 8 gives 1.45x geomean minimum bulk
617
+ ESS per gradient and 17/18 gate passes; geomean versus CmdStan 0.49x; the
618
+ full `stan_style` preset reaches 0.68x but regresses four models and fails
619
+ R-hat on two, so it is opt-in.
620
+ - Appendix C robustness (`STUDIES/paper_adaptation_robust_v1`): the v4
621
+ default is robust on all 14 previously freezing posteriordb cells and
622
+ 0.90-1.35x dual averaging's minimum bulk ESS per gradient (geomean 1.04).
623
+ - Kernel efficiency against a clean-room reference NUTS
624
+ (`STUDIES/kernel_efficiency_v1`, ESS per gradient, seed medians): the
625
+ default kernel is 0.81x on Eight Schools, 0.75x on the 100-D Gaussian and
626
+ 1.03x on the 50-D correlated Gaussian; with the initial-evaluation cache
627
+ 0.91x / 0.81x / 1.06x, and with Stan's momentum-sum U-turn rule as well
628
+ 0.86x / 1.09x / 1.07x. The gap decomposes into the wasted re-evaluation
629
+ (exact, now cached by default), the endpoint U-turn rule (0.75x on the
630
+ isotropic Gaussian, 1.0x on the correlated one) and refinement rejections
631
+ (0.85-0.95x where refinement engages), consistent with the posteriordb
632
+ 0.7-0.8x.
633
+ - Sampler API parity: every `Sampler::run` path is bit-identical to the
634
+ `walnutpie` entry point it wraps (`tests/sampler_api.rs`); the
635
+ allocation-free kernel reproduces the pinned run fingerprints in debug and
636
+ release (`tests/kernel_fingerprint.rs`).
637
+ - Diagnostics: R-hat, ESS (bulk, tail, quantile, mean) and MCSE match ArviZ
638
+ to 1e-6 relative on `tests/data/arviz_fixture.json`; `az.summary` over the
639
+ exported CmdStan CSV agrees with the Rust `Summary`.
640
+
641
+ ## [0.1.0-beta.2] - 2026-08-31
642
+
643
+ First release candidate. The kernel is at revision
644
+ `walnutpie-warmup-telemetry-tau0.6-m1-r2-e1-d3-v10`; the paper adaptation mode
645
+ is at `walnutpie-paper-adaptation-kquantile-gamma-v3`. Evidence for every claim
646
+ below is a checksummed study under `STUDIES/` with an entry in
647
+ `wiki/research-ledger-2026-08-31.md` (entry ids in brackets).
648
+
649
+ ### Fixed
650
+
651
+ - **Micro-step acceptance statistic (kernel `v9`).** Through `v8` a refinement
652
+ level was accepted when the largest Hamiltonian departure of *any* visited
653
+ micro-step from the start state was within `max_error`. That statistic is
654
+ not symmetric under time reversal, so the deterministic reverse selection
655
+ could disagree with the forward selection and non-reversible leaves were
656
+ accepted. On Neal's 10-D funnel at the paper's tuning the kernel placed about
657
+ twice the correct mass below `omega = -5` (0.0971 vs the exact 0.0478).
658
+ Acceptance now uses the endpoint departure `|H(end) - H(start)|` exactly as
659
+ upstream `walnutpie::macro_step`/`within_tolerance`. Verified by a
660
+ 4,000-leaf funnel differential oracle generated from the unmodified upstream
661
+ headers (`oracle/walnutpie/f5bba365_funnel_leaves`; `v8` disagreed on 1,555
662
+ leaves, `v9` agrees on all 4,000 to 1e-11) and by
663
+ `STUDIES/funnel_bias_fix_v1` (P(omega<-5) 0.0474 at 4×50,000 draws).
664
+ Runs that never refine are unchanged; every earlier result from a
665
+ refinement-active run is provisional until re-run.
666
+ [WP2-FUNNEL-REPRO-V1, WP6-FUNNEL-BIAS-FIX-V9]
667
+ - **Recoverable target failures refine instead of stopping (kernel `v10`).**
668
+ Through `v9` a `TargetError::recoverable` result stopped the whole transition
669
+ (`StopReason::InvalidEvaluation`), so a stiff model whose coarse micro-steps
670
+ overshoot into a non-representable region produced no-op transitions (65–100%
671
+ of transitions on the Stock–Watson study). Upstream maps a failed evaluation
672
+ to `logp = -inf`, `grad = 0` (`walnutpie/util.hpp`), so the micro-step merely
673
+ fails the endpoint tolerance and the leaf refines; only when every level
674
+ fails is the leaf rejected by refinement exhaustion. `v10` does exactly that,
675
+ excludes zero-density points from the Hamiltonian extrema and divergence
676
+ statistic, and reports them as `zero_density_evaluations` per transition and
677
+ per work partition. A successful run can no longer report
678
+ `StopReason::InvalidEvaluation` (retained in the enum). Nonfinite *returned*
679
+ values (`NaN`, `+inf`, nonfinite gradient) remain fatal. Verified by a
680
+ 4,000-leaf differential oracle with a throwing wall target
681
+ (`oracle/walnutpie/f5bba365_invalid_leaves`) and by
682
+ `STUDIES/invalid_evaluation_parity_v1` (0 invalid-evaluation stops across
683
+ 216,000 retained transitions and 8.5M recoverable evaluations; truncated
684
+ Gaussian stationary; funnel unchanged). [WP2b-SW-REPRO-V1,
685
+ WP10-INVALID-EVALUATION-PARITY-V10]
686
+ - **Paper-mode `h` statistic (paper adaptation `v2`).** The unrefined fraction
687
+ is now taken over *built* leaves, a transition without a built leaf
688
+ contributes no sample and no step update
689
+ (`PaperAdaptationUpdate::transitions_without_statistic`), and the installed
690
+ paper-mode step is bounded to `PAPER_STEP_RELATIVE_BOUND` (1e3) times the
691
+ configured initial step. Under `v1`, all-invalid transitions counted as fully
692
+ unrefined and could drive `h` to its 1e6 ceiling. [WP9]
693
+ - The pinned upstream macro-leaf oracle no longer carves out
694
+ `forward_refinement`, `backward_refinement`, and
695
+ `multi_level_reverse_coarsening`; all pinned cases pass unmodified.
696
+
697
+ ### Added
698
+
699
+ - **JMLR Appendix C adaptation**, opt-in through
700
+ `WarmupConfig::with_paper_adaptation(PaperAdaptationConfig)`: the K-quantile
701
+ rule for `delta` (`max_error`) and `Gamma`-targeted dual averaging for `h`
702
+ (`step_size`), with `PaperStepStatistic`, `PaperRestartPolicy`,
703
+ `PaperAdaptationUpdate` telemetry, `WarmupCheckpointTelemetry::
704
+ unrefined_fraction`/`max_error_after`, and `PAPER_ADAPTATION_REVISION`.
705
+ On Neal's funnel from a conservative start it is unbiased and, with the `v3`
706
+ default, 1.41×/1.61× (bulk/tail ESS per target call) more efficient than the
707
+ paper's fixed funnel tuning. [WP7-FUNNEL-ADAPTIVE-V1,
708
+ WP9-PAPER-H-RULE-STABILISATION-V2]
709
+ - Research-only outer-orbit selection switch
710
+ `RunConfig::with_research_outer_orbit_selection` (`OuterOrbitSelection::
711
+ {BiasedProgressive, ExactNormalizedMultinomial}`); the default is unchanged.
712
+ - Fixed-metric facades: `DenseMass`, `BlockDiagonalMass`,
713
+ `StructuredBlockMass` (`StructuredCovarianceBlock::{BidiagonalCholesky,
714
+ ScaledAr1}`, linear time), `LowRankArrowheadMass`, and the versioned
715
+ `sample_direct_original_q` family (`DirectOriginalQMass`,
716
+ `DIRECT_ORIGINAL_Q_REVISION`) executing dynamics in target coordinates.
717
+ - Research-only projected/pooled arrowhead warmup (`sample_projected_arrowhead`,
718
+ `sample_chains_projected_arrowhead`, `PROJECTED_ARROWHEAD_REVISION`); frozen
719
+ at its stage-7 state and not recommended for use (see
720
+ `wiki/sampler-path-ledger.md`).
721
+ - `RunConfig::with_research_target_evaluation_limit`
722
+ (`ResearchTargetEvaluationLimit`, `RESEARCH_MAX_TARGET_EVALUATIONS`) and the
723
+ explicitly budgeted entry points `sample_chains_with_target_budget` /
724
+ `preflight_chains_with_target_budget` for deep-refinement runs that exceed
725
+ the conservative admission ceiling.
726
+ - Warmup telemetry checkpoints, `DualAveragingAcceptance::AcceptedTrajectory`
727
+ (research-only), `InitialStepSearchConfig`, and trace-only depth diagnostics
728
+ (final U-turn predicate dots, margin, physical trajectory length).
729
+ - Examples `funnel_paper_adaptation` and `state_space_path_metric`; CI on
730
+ Linux/Windows GNU 1.88 and Linux stable.
731
+
732
+ ### Changed
733
+
734
+ - **Paper-mode default restart policy (paper adaptation `v3`).**
735
+ `PaperAdaptationConfig::default()` continues one dual-averaging stream
736
+ across `delta` installations (`PaperRestartPolicy::
737
+ ContinueThroughLocalErrorInstall`); restarting left chain-specific final
738
+ steps (spread 1.7–2.8×) while continuing gave ≤ 1.3× with equal or better
739
+ efficiency in both tested `Delta` families. Select
740
+ `RestartOnLocalErrorInstall` to reproduce the `v1`/`v2` behaviour.
741
+ [WP9-PAPER-H-RULE-STABILISATION-V2]
742
+ - `ALGORITHM_REVISION` advanced `v8 → v9 → v10`. Seeds are not portable across
743
+ revisions; both changes leave the frozen default tuning bit-identical (no
744
+ pinned fingerprint changed) because it never reaches a leaf where the old and
745
+ new rules differ.
746
+ - Crate description and README rewritten for a first-time reader; the
747
+ "internal beta" framing is replaced by an explicit validated-results table
748
+ and known-limitations list.
749
+
750
+ ### Validation (2026-08-31 program)
751
+
752
+ - Neal's funnel, paper tuning (δ = 0.21, h = 0.36, WALNUTS-D, depth 10, 10
753
+ levels), 4×50,000 draws: P(omega<-5) 0.0474 vs exact 0.0478 (z −0.08),
754
+ var(omega) 9.04 vs 9, zero divergences/invalid/exhaustions; the upstream
755
+ reference at identical tuning gives 0.0477. [WP6-FUNNEL-BIAS-FIX-V9]
756
+ - Neal's funnel, Appendix C warmup from δ = 1, h = 0.1 (`v3` default): all
757
+ gates pass, final-step spread ≤ 1.27× across chains, 1.41×/1.61× bulk/tail
758
+ ESS per call versus fixed paper tuning. [WP9-PAPER-H-RULE-STABILISATION-V2]
759
+ - Noncentered Eight Schools, v38 strict track, kernel `v9`: conservative
760
+ minimum over seven seeds and six functionals 12,830 bulk / 10,346 tail ESS/s
761
+ (CmdStan 6,290 / 3,951; BlackJAX 5,645 / 4,195; NumPyro 5,241 / 4,050); ESS
762
+ per target call unchanged from `v7` (0.96 / 0.99). Walls were measured on a
763
+ loaded machine. [WP8-EIGHT-SCHOOLS-V9-REBENCH-V1]
764
+ - Outer-selection reverse ablation on Eight Schools: biased progressive
765
+ selection (the default) gives 1.75× bulk ESS per target call over exact
766
+ normalized multinomial with no tail penalty. [WP3-1]
767
+ - Exact Gaussian state-space ground truth, T ∈ {100, 1000}: the
768
+ posterior-precision tridiagonal path metric mixes at depth 3–4 at
769
+ Monte-Carlo accuracy (ESS/call 4.8× identity, ~1,000× a prior-based metric,
770
+ which caps at depth 8 in 92% of transitions at T = 1000). [WP4-ESSGT-V1]
771
+ - Real Polyscope state-space target at T = 1000 (non-pathological fixture):
772
+ oWALNUTS with an adapted diagonal in centered coordinates passes every gate
773
+ NumPyro NUTS passes, confirmed on three fresh seeds; the posterior-precision
774
+ path block agrees on every seed and gives 2.7× ESS per call but passed the
775
+ strict conjunctive gate on 2/3 seeds (not yet confirmed)
776
+ [WP12-SSPD11-CONFIRMATION-V1]. On the σ_x → 0
777
+ funnel fixture no tested Euclidean sampler passes, NumPyro included (1,510
778
+ divergences). [WP4B-REAL-TARGET-PATH-METRIC-V1]
779
+ - Stock–Watson stochastic volatility (simulated series): the paper's fixed
780
+ tuning does not reproduce the paper's energy-error contrast on this series;
781
+ the Appendix C adaptation arm passes every gate on 2/3 fresh seeds at
782
+ 4×2,000 draws (miss: R-hat 1.0101, clean health) and is 2.0× more efficient
783
+ per call than the fixed tuning. [WP2b-SW-REPRO-V1,
784
+ WP12-SSPD11-CONFIRMATION-V1]
785
+
786
+ ### Errata
787
+
788
+ - The Eight Schools figures previously circulated as oWALNUTS's "conservative
789
+ minimum across seeds and six functionals" (19,054.65 bulk / 14,494.34 tail
790
+ ESS/s) were the minimum over functionals of the across-seed *median*, while
791
+ competitor figures in the same table were true minima. The like-for-like
792
+ `v7` minimum was 8,634 / 5,949 ESS/s (still fastest among the strict matched
793
+ competitors, 1.37× / 1.42×). The corrected `v9` numbers are above.
794
+ [WP8-EIGHT-SCHOOLS-V9-REBENCH-V1]
795
+
796
+ ## [0.1.0-beta.1] - internal, never published
797
+
798
+ ### Added
799
+
800
+ - Minimal standalone `owalnuts` crate containing the fixed-diagonal internal-beta facade.
801
+ - Bounded Gaussian example, facade tests, resource controls, telemetry, and run identity metadata.
802
+ - Private parity tests and pinned upstream oracle fixtures with provenance.
803
+ - Opt-in acceptance-driven warmup (dual-averaged step, Welford diagonal mass)
804
+ and configurable `KernelTuning`.
805
+
806
+ ### Removed
807
+
808
+ - Legacy weighted sampler, NUTS, Python, benchmark, and prototype public surfaces.