|
| 1 | +import unittest |
| 2 | + |
| 3 | +import numpy as np |
| 4 | + |
| 5 | +from causallearn.search.PermutationBased.gst import GST |
| 6 | + |
| 7 | + |
| 8 | +# Deterministic local scores for vertex 0 over candidate parents {1, 2, 3}, |
| 9 | +# taken from py-why/causal-learn#264. BOSS uses a higher-is-better score. |
| 10 | +# S({1}) and S({2}) use the issue's reported trace values; the remaining |
| 11 | +# entries use the issue's local-score table. |
| 12 | +_LOCAL_SCORES = { |
| 13 | + frozenset(): 0.0, |
| 14 | + frozenset({1}): 2.0837, |
| 15 | + frozenset({2}): 4.3568, |
| 16 | + frozenset({3}): 1.53, |
| 17 | + frozenset({1, 2}): -0.03, |
| 18 | + frozenset({1, 3}): 3.29, |
| 19 | + frozenset({2, 3}): 2.33, |
| 20 | + frozenset({1, 2, 3}): -0.25, |
| 21 | +} |
| 22 | + |
| 23 | + |
| 24 | +class _MockScore: |
| 25 | + """Minimal score object exposing only the interface that GST relies on.""" |
| 26 | + |
| 27 | + def __init__(self, n_vars, scores): |
| 28 | + # GST reads data.shape[1] to enumerate the candidate parents. |
| 29 | + self.data = np.zeros((1, n_vars)) |
| 30 | + self._scores = scores |
| 31 | + |
| 32 | + def score(self, i, PAi): |
| 33 | + return self._scores[frozenset(PAi)] |
| 34 | + |
| 35 | + def score_nocache(self, i, PAi): |
| 36 | + return self._scores[frozenset(PAi)] |
| 37 | + |
| 38 | + |
| 39 | +class TestGST(unittest.TestCase): |
| 40 | + """Regression test for GST grow-branch ordering (py-why/causal-learn#264). |
| 41 | +
|
| 42 | + BOSS uses a higher-is-better score, and TETRAD's GrowShrinkTree visits grow |
| 43 | + branches in descending growScore order. The trace path -- not just the |
| 44 | + presentation order -- depends on this direction, so the highest-scoring grow |
| 45 | + branch reachable within the prefix must be visited first. |
| 46 | + """ |
| 47 | + |
| 48 | + def test_grow_branches_sorted_descending(self): |
| 49 | + gst = GST(0, _MockScore(4, _LOCAL_SCORES)) |
| 50 | + # Force the root to grow over all candidates {1, 2, 3}. |
| 51 | + gst.trace([1, 2, 3]) |
| 52 | + grow_scores = [branch.grow_score for branch in gst.root.branches] |
| 53 | + self.assertEqual(grow_scores, sorted(grow_scores, reverse=True)) |
| 54 | + # 2 (4.3568) > 1 (2.0837) > 3 (1.53) |
| 55 | + self.assertEqual([branch.add for branch in gst.root.branches], [2, 1, 3]) |
| 56 | + |
| 57 | + def test_trace_follows_highest_scoring_branch(self): |
| 58 | + # With prefix [1, 2], both {1} and {2} are reachable grow branches. |
| 59 | + # Descending order (TETRAD-style) visits {2} first -> parents {2}. |
| 60 | + # Ascending order (the pre-fix behavior) would visit {1} -> parents {1}. |
| 61 | + gst = GST(0, _MockScore(4, _LOCAL_SCORES)) |
| 62 | + parents = [] |
| 63 | + score = gst.trace([1, 2], parents) |
| 64 | + self.assertEqual(parents, [2]) |
| 65 | + self.assertAlmostEqual(score, 4.3568, places=6) |
| 66 | + |
| 67 | + |
| 68 | +if __name__ == "__main__": |
| 69 | + unittest.main() |
0 commit comments