Skip to content

Visit GST grow branches in descending score order (BOSS/GRaSP)#265

Merged
kunwuz merged 1 commit into
py-why:mainfrom
sanghack81:fix/gst-branch-sort-descending
Jun 4, 2026
Merged

Visit GST grow branches in descending score order (BOSS/GRaSP)#265
kunwuz merged 1 commit into
py-why:mainfrom
sanghack81:fix/gst-branch-sort-descending

Conversation

@sanghack81

Copy link
Copy Markdown
Contributor

Summary

Fixes the grow-branch sort direction in causallearn/search/PermutationBased/gst.py so the Grow-Shrink Tree visits higher-scoring grow branches first, matching TETRAD's GrowShrinkTree (discussed in #264).

Root cause

BOSS uses a higher-is-better score. In TETRAD's edu.cmu.tetrad.search.utils.GrowShrinkTree, GSTNode.compareTo() orders by ascending growScore, but grow() sorts with this.branches.sort(Collections.reverseOrder()), so branches are visited in descending growScore order.

In causal-learn, GSTNode.__lt__ is ascending and grow() called self.branches.sort() — i.e. ascending, the opposite direction.

Why it matters (not just presentation order)

GST.trace descends into the first prefix-matching branch and returns its shrink score, so the sort direction changes the returned score and parent set for a given prefix. With the deterministic mock scores from #264 and prefix [1, 2]:

ascending  -> score 2.0837, parents {1}
descending -> score 4.3568, parents {2}

Visiting the best improving add first follows the more promising path — a better local approximation that also prunes more of the tree.

Change

# grow()
self.branches.sort(reverse=True)

I kept __lt__ ascending and reversed at the sort site, mirroring TETRAD's compareTo + Collections.reverseOrder() (equivalent to inverting the comparator, the other form proposed in #264). This GST is shared by both boss and grasp, so both now use the descending (TETRAD-style) traversal.

Scope

This PR changes only the branch traversal order. Other existing behavior is intentionally left as-is: grow branches are still included on strict improvement (score > self.grow_score; TETRAD uses >=), and background knowledge (forbidden/required) remains unimplemented as before. Aligning those with TETRAD is out of scope here.

Performance (from #264; synthetic linear-Gaussian, BIC-from-cov, avg_degree=2, lambda=2)

p=50, n=1000:

seed current ascending TETRAD-style descending speedup CPDAG edges
101 1.376s 0.269s 5.12x 48 / 48
102 0.806s 0.236s 3.42x 44 / 44
103 1.543s 0.315s 4.91x 46 / 46
104 0.714s 0.209s 3.42x 40 / 40

Mean wall time 1.110s → 0.257s (4.32x).

p=100, n=1000:

seed current ascending TETRAD-style descending speedup CPDAG edges
101 3.603s 1.107s 3.25x 86 / 86
102 10.686s 1.816s 5.89x 111 / 110
103 13.717s 1.753s 7.82x 108 / 108
104 6.377s 1.344s 4.75x 101 / 102

Mean wall time 8.596s → 1.505s (5.71x). As noted in #264, in some seeds the returned CPDAG edge count also changes.

Test

Adds tests/TestGST.py, a focused, deterministic regression test over the #264 mock scores: it asserts the root grow branches are ordered by descending score and that tracing prefix [1, 2] selects the higher-scoring parent {2}. The test fails on the previous ascending behavior and passes after this change.

Closes #264

BOSS uses a higher-is-better score, and TETRAD's GrowShrinkTree visits
grow branches in descending growScore order (GSTNode.compareTo() is
ascending, but grow() sorts with Collections.reverseOrder()).
causal-learn's GST sorted them ascending, so the trace followed the
lowest-scoring improving add first.

Because GST.trace descends into the first prefix-matching branch and
returns its shrink score, the sort direction changes the returned score
and parent set for a given prefix -- not just presentation order. With
the issue's mock scores, prefix [1, 2] returns {1} (2.0837) ascending vs
{2} (4.3568) descending. Visiting the best improving add first follows
the more promising path: a better local approximation that also prunes
more, matching TETRAD. This GST is shared by both BOSS and GRaSP.

Adds tests/TestGST.py, a focused regression test over the deterministic
mock scores from the issue.

Refs py-why#264

Signed-off-by: Sanghack Lee <sanghack.lee@gmail.com>
@sanghack81
sanghack81 force-pushed the fix/gst-branch-sort-descending branch from 708cb8a to 140a858 Compare May 30, 2026 07:46
@kunwuz

kunwuz commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Thanks so much for your contribution!

@kunwuz
kunwuz merged commit 8f479e3 into py-why:main Jun 4, 2026
2 checks passed
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.

BOSS GST branch sort direction may differ from TETRAD

2 participants