Skip to content

feat: determinant-tilt prior on the continuous-precision block#106

Merged
MaartenMarsman merged 10 commits into
mainfrom
feature/graph-precision-priors
May 19, 2026
Merged

feat: determinant-tilt prior on the continuous-precision block#106
MaartenMarsman merged 10 commits into
mainfrom
feature/graph-precision-priors

Conversation

@MaartenMarsman
Copy link
Copy Markdown
Collaborator

@MaartenMarsman MaartenMarsman commented May 19, 2026

Summary

Adds the determinant-tilt prior $|K|^{\delta}$ as an optional modifier on the continuous-precision block of both the GGM and the mixed-MRF Kyy. With $\delta = 0$ (default) the prior is unchanged; with $\delta > 0$, the prior gains the factor $|K|^{\delta}$, which softly repels the chain from the positive-definite cone boundary. Motivated by the determinant-tilted spike-and-slab construction of the companion paper at ~/SV/spikeslab/, which documents the cone-interior collapse of the unmodified discrete spike-and-slab at moderate-to-large $q$ and demonstrates that $\delta \approx 1$ recovers a usable prior bulk in that regime.

The default is intentionally $\delta = 0$ in this PR so the change is fully backward-compatible. A follow-up PR will introduce a dimension-adaptive default (δ(q) = c · log q, per the companion paper's empirical scaling rule); see dev/plans/backlog/hierarchical-ggm-degord-rr.md Stage 1.

Changes

C++ — GGM (src/models/ggm/)

  • ggm_gradient.{h,cpp}: NUTS gradient adds delta * d log|K| / d theta along the Cholesky parameterisation.
  • ggm_model.{h,cpp}: MH paths (edge-toggle, edge-update, diagonal) add delta * (log|K_prop| - log|K_curr|) via the rank-1/rank-2 matrix-determinant lemma in O(p)/O(1). New set_determinant_tilt() accessor; forwarded to the gradient engine on rebuild.

C++ — Mixed MRF (src/models/mixed_mrf/)

R API

  • bgm() gains delta = 0 argument with roxygen documenting the partial-association convention and the PD-cone-boundary rationale.
  • Not allowed for pure ordinal models (no continuous precision to tilt) — checked in the validator.
  • sample_ggm_prior() (renamed from sample_precision_prior in 987cc8b) gains the same delta argument.
  • man/bgm.Rd regenerated.

Tests (gated behind BGMS_RUN_SLOW_TESTS)

  • test-sbc-ggm.R: new SBC test at $\delta = 1$ with a tilt-aware rejection sampler (shifted-shape Gamma proposal + $(|K|/\prod K_{ii})^{\delta}$ acceptance per the companion paper §sec:tilt-empirics).
  • test-ggm-nuts.R: new NUTS-vs-MH posterior moment-concordance test at $\delta = 1$.
  • test-mixed-nuts.R: new M.2T NUTS-vs-MH concordance for the Kyy block at $\delta = 1$.
  • Existing $\delta = 0$ tests untouched; verified bit-identical numerics under the default.

Test plan

  • R CMD check --as-cran: 0 errors / 0 warnings / 0 notes (local, 4m 20s).
  • Full slow test suite (BGMS_RUN_SLOW_TESTS=true devtools::test() on sbc-ggm|ggm-nuts|mixed-nuts): 1196/1196 assertions, 0 failures, 0 skips (2.4 min).
  • $\delta = 0$ posterior numerics unchanged from main (no regression).
  • NUTS vs MH concordance holds at $\delta = 1$ for both GGM and mixed-MRF Kyy.
  • CI green.
  • Tilt-specific scenario (e.g., near-singular Kyy at $\delta = 1$) on the user's data.

Companion / follow-up

  • Companion paper: ~/SV/spikeslab/manuscript.tex (determinant-tilted spike-and-slab construction; dimension-adaptive scaling rule).
  • Follow-up Stage 1: auto-default δ(q) = 0.5 · log(q) exposed via delta = NULL.
  • Follow-up Stage 2: sample_ggm_prior(spec = "joint") for SBC under the joint specification.
  • Stage 3 (medium-term): hierarchical-spec GGM inference via DEGORD + Russian-Roulette.

See dev/plans/backlog/hierarchical-ggm-degord-rr.md for the full multi-stage plan.

Adds a non-negative tilt parameter delta to the GGM gradient engine:
multiplies the prior by |K|^delta, which in Cholesky coordinates is
+ 2*delta * sum(psi). One line on the log-posterior in each gradient
path (logp_and_gradient, logp_and_gradient_full) and +2*delta on each
psi-bar entry in the adjoint. Cost O(p) per leapfrog step inside an
O(p^3) gradient; effectively free.

delta is plumbed through GGMGradientEngine::rebuild and stored on the
engine. GGMModel exposes a set_determinant_tilt(delta) setter that
forwards on the next ensure_constraint_structure() rebuild; the value
is also carried in the copy constructor so per-chain clones target the
intended posterior.

Currently consumed only by the NUTS path. The MH ratios in ggm_model.cpp
do not yet include the corresponding +delta * (log|K_prop| - log|K_curr|)
term, so this is safe only when no MH moves fire (i.e. sample_ggm_prior
with edge_selection = false). The MH-path tilt will land before delta
is exposed on bgm().
The function samples from a prior whose density is specified on the
partial-association scale K_yy = -K/2, not on the raw precision K, so
"precision" was misleading. Output samples are still entries of K.
Renames the R wrapper, the Rcpp export (sample_ggm_prior_cpp), the
internal C++ function, the test file, and the docstring; regenerates
RcppExports and roxygen docs to match.
Parallel of D for GGM. Adds a non-negative tilt parameter delta to the
mixed-MRF Kyy block: multiplies the prior by |Kyy|^delta, which in
Cholesky coordinates is + 2*delta * sum(psi). Two added lines per
gradient path (logp and psi_bar) in mixed_mrf_gradient.cpp; cost is
O(q) per leapfrog step inside an O(q^3) gradient.

MixedMRFModel gains a determinant_tilt_yy_ member (default 0.0) and a
set_determinant_tilt_yy() setter. The value is carried in the copy
constructor so per-chain clones see the intended posterior. Currently
consumed only by the NUTS gradient paths; the continuous-block MH
ratios in mixed_mrf_metropolis.cpp are not yet updated.

mixed_test_logp_and_gradient_full grows a delta argument so the tilt
is exercisable from R. Finite-difference tests added to
test-mixed-gradient-pfaffian.R cover:
  - delta = 0 bit-identical to no-tilt call
  - delta > 0 on full Kyy
  - delta > 0 on sparse Kyy with random diagonal mass
  - analytic check: tilted logp - untilted logp == delta * log|Kyy|
All pass at max relative error < 1e-5.

RcppExports regenerated via Rcpp::compileAttributes(). No regressions
in the full test suite (6708 pass, 0 fail).
Companion to commit e4f24cb (NUTS-side GGM tilt). Adds the matching
delta * (log|K_prop| - log|K_curr|) term to all six GGMModel MH-ratio
sites:
  - update_edge_parameter
  - update_diagonal_parameter
  - update_edge_indicator_parameter_pair (both branches)
  - tune_proposal_sd (off-diagonal and diagonal passes)

The log-determinant ratio is computed in O(p) for rank-2 edge updates
and O(1) for rank-1 diagonal updates via the matrix-determinant lemma,
reusing the cached covariance_matrix_ and the already-set-up
precision_proposal_. Existing log_density_impl_edge/diag are refactored
to share two new private helpers log_det_ratio_edge/diag; both helpers
return exactly the logdet term that those functions previously computed
inline, so the refactor is observationally inert at delta = 0.

Each tilt addition is guarded by `if (determinant_tilt_ != 0.0)` so the
existing default code path remains a single-pass MH ratio with no
additional log() call. NUTS and MH paths now both consume
determinant_tilt_ consistently; the chain targets the same posterior
under either sampler.

No regressions in the existing test suite (6708 pass, 0 fail).
Companion to commit 1bb74c7 (NUTS-side mixed-MRF tilt). Adds the
matching delta * (log|Kyy_prop| - log|Kyy_curr|) term to the three
continuous-block MH-ratio sites in MixedMRFModel:
  - update_pairwise_effects_continuous_offdiag
  - update_pairwise_effects_continuous_diag
  - update_edge_indicator_continuous

Adds two private helpers log_det_ratio_yy_edge/diag that compute the
log-determinant ratio via the matrix-determinant lemma in O(q) for
rank-2 and O(1) for rank-1, reusing covariance_continuous_ and the
already-set-up precision_proposal_. The helpers mirror
GGMModel::log_det_ratio_edge/diag.

Each tilt addition is guarded by `if (determinant_tilt_yy_ != 0.0)`
so the default delta_yy = 0 path is bit-identical to the previous
code. NUTS and MH now both consume determinant_tilt_yy_ consistently;
the chain targets the same posterior under either sampler.

No regressions in the existing test suite (6708 pass, 0 fail).
Adds a delta argument to bgm() that propagates the determinant tilt to
both the NUTS and adaptive-Metropolis paths for GGM and mixed-MRF
models. delta = 0 is the default (untilted) and reproduces the current
behaviour bit-for-bit.

R-layer plumbing:
  bgm(delta = 0)
  -> bgm_spec(delta = 0)             # validation + dispatch
  -> build_spec_{ggm,mixed_mrf}      # stored on spec$prior$delta
  -> run_sampler_{ggm,mixed_mrf}     # forwards to C++ entry point
  -> sample_{ggm,mixed_mrf}(delta = ...)

C++-layer plumbing:
  sample_ggm        calls model.set_determinant_tilt(delta)
  sample_mixed_mrf  calls model.set_determinant_tilt_yy(delta)

bgm_spec rejects delta > 0 for pure-ordinal models (model_type "omrf"
or "compare") since there's no precision matrix to tilt. Negative,
non-finite, or non-scalar delta values produce a clear error early in
bgm_spec().

New test file test-bgm-delta.R covers:
  - NUTS path: delta > 0 shifts tr(K) upward vs delta = 0 (GGM)
  - MH path:   delta > 0 shifts tr(K) upward vs delta = 0 (GGM)
  - Pure-ordinal model rejects delta > 0 with informative message
  - Invalid delta (negative, NA, non-scalar) rejected at the validator

RcppExports regenerated via Rcpp::compileAttributes(). Full suite:
6714 pass, 0 fail.
The copy constructor initialised pcg_lambda_cache_ before
determinant_tilt_, but determinant_tilt_ is declared first in the class.
C++ always initialises in declaration order, so the listing was
misleading and triggered -Wreorder-ctor across every translation unit
that includes ggm_model.h.
The previous reorder fix swapped the trailing pair but left
determinant_tilt_ near the end of the init list. determinant_tilt_ is
declared just after target_accept_ near the top of the private section,
so the init list still triggered -Wreorder-ctor against theta_. Move
determinant_tilt_ to immediately after target_accept_ to match.
Three new slow tests gated behind BGMS_RUN_SLOW_TESTS:

- GGM NUTS SBC at delta=1 (p=3, R=200), with a tilt-aware prior
  sampler using the shifted-shape Gamma proposal + (|K|/prod(K_ii))^delta
  acceptance from the spikeslab manuscript §sec:tilt-empirics.
- GGM NUTS-vs-MH posterior moment concordance at delta=1 (p=4).
- Mixed-MRF NUTS-vs-MH posterior concordance at delta=1 (Kyy block,
  no edge selection).

Regenerate man/bgm.Rd via devtools::document() to pick up the delta
parameter exposed by 8a9f7cb.
Commit 987cc8b renamed sample_precision_prior to sample_ggm_prior but
left the topic reference in _pkgdown.yml pointing at the old name,
causing build_reference_index() to fail on the missing topic.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

❌ Patch coverage is 71.05263% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.36%. Comparing base (e0943be) to head (243602d).

Files with missing lines Patch % Lines
src/models/mixed/mixed_mrf_metropolis.cpp 3.57% 27 Missing ⚠️
src/models/ggm/ggm_model.cpp 88.88% 4 Missing ⚠️
R/sample_ggm_prior.R 85.71% 1 Missing ⚠️
src/ggm_gradient_interface.cpp 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #106      +/-   ##
==========================================
+ Coverage   86.52%   87.36%   +0.83%     
==========================================
  Files          77       77              
  Lines       13354    13441      +87     
==========================================
+ Hits        11555    11743     +188     
+ Misses       1799     1698     -101     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@MaartenMarsman MaartenMarsman merged commit cb68f0e into main May 19, 2026
9 of 10 checks passed
@MaartenMarsman MaartenMarsman deleted the feature/graph-precision-priors branch May 19, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant