Skip to content

Implement Balanced Up-Down Walk (Akitaya et al 2026)#211

Open
christopherkenny wants to merge 12 commits into
devfrom
bud
Open

Implement Balanced Up-Down Walk (Akitaya et al 2026)#211
christopherkenny wants to merge 12 commits into
devfrom
bud

Conversation

@christopherkenny

@christopherkenny christopherkenny commented Mar 2, 2026

Copy link
Copy Markdown
Member

This builds off of the Cycle Walk implementation, so the code is a bit less spread across files.

Paper is from a few weeks ago: https://arxiv.org/abs/2602.11993

Covered here are:

  • standard algorithm
  • weighted edge support using the same approach as cyclewalk
  • integration with our MH system
  • consistency with our diagnostics
  • support for shortbursts
  • consistent tests + validations with cyclewalk

7x7 validation:
image

The root cause was that get_district_path returned DistrictPair{min,max},
which loses the directed order of the district path. For a path like
2->1->0, edges were stored as {1,2},{0,1} causing districts to be
extracted as [1,0,1] instead of [2,1,0]. This led to duplicate district
assignments, silently dropped marked edges, and systematic bias.

Fix: Store district_path as directed pair<int,int> preserving {from,to}
order. Convert to DistrictPair only for marked_edges map lookup.

Other changes:
- Add BUD diagnostics (accept/reject/trivial) to summary output
- Add 'bud' to supported algorithms in redist_ci
- Remove debug-only compactness==98/99 special modes
- Remove compute_log_wst_from_plan dead code
- Add translations.md documenting Julia-to-C++ review findings

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new MCMC backend implementing the “Balanced Up-Down Walk (BUD)” sampler and integrates it into the existing redist workflow (shortburst optimization, diagnostics, edge-weight validation, and tests), largely mirroring the existing CycleWalk structure.

Changes:

  • Introduces a full BUD sampler stack (C++ core + R wrapper) with support for constraints, compactness, multiple chains, and edge-weight validation.
  • Integrates BUD into redist_shortburst(), summary.redist_plans(), and redist_ci().
  • Adds a comprehensive test suite (including long-run distribution checks, skipped on CRAN/CI) and exploratory validation script(s).

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/testthat/test_burst.R Adds shortburst tests for the new backend = "bud".
tests/testthat/test-redist_bud_weights.R Adds edge-weight validation + behavior tests for redist_bud().
tests/testthat/test-redist_bud_long.R Adds long-running distribution validation tests for BUD (skipped on CRAN/CI).
tests/testthat/test-redist_bud_chains.R Adds multi-chain and init-plan behavior tests for BUD.
tests/testthat/test-redist_bud.R Adds core correctness/sanity tests for BUD sampling output.
src/cw_partition.h Makes LCTPartition internals protected for subclassing (needed by BUD partition).
src/cw_lct.cpp Reworks reversal propagation to be iterative and adds loop guards.
src/bud_proposal.h Declares BUD proposal/step interfaces and data structures.
src/bud_proposal.cpp Implements the BUD proposal and MH step, including compactness/constraint adjustments.
src/bud_partition.h Adds BUD-specific partition state (district tree, marked edges, cycle LCT, cuttable info).
src/bud_partition.cpp Implements BUD partition initialization and cuttable-info computations.
src/bud_main.h Declares the BUD entry point for the Rcpp-exposed MCMC loop.
src/bud_main.cpp Implements the BUD MCMC loop (bud_plans) analogous to CycleWalk.
src/RcppExports.cpp Registers _redist_bud_plans for R access.
man/redist_shortburst.Rd Documents new "bud" backend option in shortburst.
man/redist_bud.Rd Adds man page for redist_bud().
explore/validate_bud_7x7.R Adds an exploratory validation script for 7x7 diagnostics/plots.
R/redist_shortburst.R Adds "bud" backend and calls into bud_plans().
R/redist_bud.R Adds the public R wrapper for BUD with chains/warmup/thin/edge-weights/constraints.
R/diagnostics.R Extends summary output to include BUD-specific diagnostics.
R/confint.R Allows redist_ci() to accept "bud" algorithm outputs.
R/RcppExports.R Adds the R binding for bud_plans().
NAMESPACE Exports redist_bud.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cw_lct.cpp
Comment on lines +179 to +183
int guard = 0;
while (n->path_parent != nullptr) {
if (++guard > 10000) {
throw std::runtime_error("LCT expose: infinite loop detected");
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop guard is hard-coded to 10000; for large instances it may throw even when path_parent chains are valid. Prefer a bound derived from the number of nodes (size()) or explicit cycle detection.

Copilot uses AI. Check for mistakes.
Comment thread src/cw_lct.cpp
Comment on lines +251 to +255
int guard = 0;
while (r->children[0] != nullptr) {
if (++guard > 10000) {
throw std::runtime_error("LCT find_root: infinite loop detected");
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 10000-iteration guard can fail on large graphs where the left spine of the splay tree legitimately exceeds 10k. Use a bound tied to size() or explicit cycle detection instead of a constant.

Copilot uses AI. Check for mistakes.
Comment thread src/cw_lct.cpp
Comment on lines +291 to +295
int guard = 0;
while (root->parent != nullptr) {
if (++guard > 10000) {
throw std::runtime_error("LCT find_path: infinite loop detected");
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard-coded guard (10000) can throw on valid large trees. If the intent is to prevent infinite loops, prefer guarding by size() or detecting cycles explicitly so this remains correct at scale.

Copilot uses AI. Check for mistakes.
Comment thread src/bud_proposal.cpp
Comment on lines +742 to +745
if (randSamp > bcuts.pathWeights[bcuts.pathWeights.size() - 2]) {
cut_district_path(partition, district_path);
return {0.0, update};
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check rejects proposals whenever randSamp falls in the last interval of pathWeights (and if there is only 1 balanced cut, it rejects 100% of the time). Since cumWeight includes all balanced cuts, this introduces an unintended rejection probability and can prevent any proposal from being selected. Likely fix: remove this guard entirely, or compare against the final cumulative weight (pathWeights.back()) / adjust the sampling logic so every balanced cut has a chance to be selected.

Suggested change
if (randSamp > bcuts.pathWeights[bcuts.pathWeights.size() - 2]) {
cut_district_path(partition, district_path);
return {0.0, update};
}

Copilot uses AI. Check for mistakes.
Comment thread R/redist_shortburst.R Outdated
Comment thread man/redist_shortburst.Rd Outdated
Comment thread src/cw_lct.cpp
Comment on lines +71 to +72
while (cur != nullptr) {
if (++guard > 10000) {

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard-coded guard limit (10000) can throw on legitimately large splay trees / large maps (e.g., >10k vertices) even when the structure is valid. If you want a safety check against cycles, prefer guarding by the maximum possible chain length (e.g., size()), or detect cycles explicitly (visited set / tortoise-hare), rather than a fixed constant.

Suggested change
while (cur != nullptr) {
if (++guard > 10000) {
int maxSteps = size();
while (cur != nullptr) {
if (++guard > maxSteps) {

Copilot uses AI. Check for mistakes.
Comment thread src/cw_lct.cpp
Comment on lines +128 to +132
int guard = 0;
while (n->parent != nullptr) {
if (++guard > 10000) {
throw std::runtime_error("LCT splay: infinite loop detected");
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixed iteration guard (10000) can incorrectly trigger on valid inputs for large trees/maps. Consider bounding by size() (or a stricter proven invariant), or switch to explicit cycle detection so this never throws purely due to scale.

Copilot uses AI. Check for mistakes.
christopherkenny and others added 2 commits March 23, 2026 08:13
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.

2 participants