bits = new HashMap<>();
+ computeBits(tree.getRoot(), bits);
+ double logp = 0.0;
+ for (Node v : tree.getNodesAsArray()) {
+ if (v.isLeaf()) {
+ continue;
+ }
+ logp += logSplitProbability(bits.get(v),
+ bits.get(v.getChildren().get(0)),
+ bits.get(v.getChildren().get(1)),
+ b0, b1, b2);
+ }
+ return logp;
+ }
+
+ /**
+ * Log conditional probability of the bipartition {@code {aBits, bBits}} of clade {@code cBits},
+ * for a clade that need not be observed. This is the whole model: every internal node of a tree
+ * contributes exactly one such factor.
+ */
+ double logSplitProbability(BitSet cBits, BitSet aBits, BitSet bBits,
+ double b0, double b1, double b2) {
+ double[] wz = logWeightsAndZ(cBits, b0, b1, b2);
+ int cls = splitClass(cBits, aBits, bBits);
+ double fS = (cls == 0)
+ ? observedPartition(getClade(cBits), aBits, bBits).getNumberOfOccurrences() : 0.0;
+ double logNumerator = (fS > 0) ? Math.log(fS + Math.exp(wz[cls])) : wz[cls];
+ return logNumerator - Math.log(wz[4]);
+ }
+
+ /**
+ * Which of the four classes the bipartition {@code {aBits, bBits}} of {@code cBits} belongs to,
+ * as a 0-based index (0 = observed split, 3 = neither child observed).
+ */
+ int splitClass(BitSet cBits, BitSet aBits, BitSet bBits) {
+ if (observedPartition(getClade(cBits), aBits, bBits) != null) {
+ return 0;
+ }
+ boolean aObs = getClade(aBits) != null;
+ boolean bObs = getClade(bBits) != null;
+ return (aObs && bObs) ? 1 : ((aObs || bObs) ? 2 : 3);
+ }
+
+ private CladePartition observedPartition(Clade c, BitSet aBits, BitSet bBits) {
+ if (c == null) {
+ return null;
+ }
+ Clade ca = getClade(aBits);
+ Clade cb = getClade(bBits);
+ if (ca == null || cb == null) {
+ return null;
+ }
+ return c.getCladePartition(ca, cb);
+ }
+
+ /* ----------------------------------------------------------------------
+ * Class sizes
+ * ------------------------------------------------------------------- */
+
+ /**
+ * Sizes {@code {|A_1|, |A_2|, |A_3|, |A_4|}} of the four split classes of clade {@code cBits}.
+ *
+ * {@code |A_1|} is the number of observed splits; a single pass over the observed subclades of
+ * {@code C} yields {@code |A_3|} (observed subclade whose complement is not observed) and the
+ * number of observed-clade pairs, from which {@code |A_2|} follows; {@code |A_4|} is the
+ * remainder of {@code 2^(m-1) - 1}. Cached, and independent of the pseudocounts.
+ */
+ double[] classSizes(BitSet cBits) {
+ double[] cached = sizeCache.get(cBits);
+ if (cached != null) {
+ return cached;
+ }
+ int m = cBits.cardinality();
+ Clade c = getClade(cBits);
+
+ double n1 = 0.0;
+ if (c != null) {
+ for (CladePartition p : c.getPartitions()) {
+ if (p.getNumberOfOccurrences() > 0) {
+ n1++;
+ }
+ }
+ }
+
+ // one pass over observed clades strictly inside C
+ int bothObservedEnds = 0; // counts each both-observed bipartition twice (once per side)
+ double n3 = 0.0;
+ for (BitSet d : sortedCladeBits()) {
+ if (d.cardinality() >= m || !subset(d, cBits)) {
+ continue;
+ }
+ BitSet complement = BitSet.newBitSet(cBits);
+ complement.andNot(d);
+ if (getClade(complement) != null) {
+ bothObservedEnds++;
+ } else {
+ n3++;
+ }
+ }
+ double n2 = Math.max(0.0, bothObservedEnds / 2.0 - n1);
+
+ double total = Math.pow(2.0, m - 1) - 1.0;
+ double n4 = Math.max(0.0, total - n1 - n2 - n3);
+
+ double[] size = {n1, n2, n3, n4};
+ sizeCache.put(BitSet.newBitSet(cBits), size);
+ return size;
+ }
+
+ private synchronized List sortedCladeBits() {
+ if (sortedCladeBits == null) {
+ List all = new ArrayList<>();
+ for (Clade c : getClades()) {
+ all.add(c.getCladeInBits());
+ }
+ all.sort(CRegCCD::compareBitSets);
+ sortedCladeBits = all;
+ }
+ return sortedCladeBits;
+ }
+
+ private BitSet computeBits(Node v, Map bits) {
+ BitSet b = BitSet.newBitSet(leafArraySize);
+ if (v.isLeaf()) {
+ b.set(v.getNr());
+ } else {
+ b.or(computeBits(v.getChildren().get(0), bits));
+ b.or(computeBits(v.getChildren().get(1), bits));
+ }
+ bits.put(v, b);
+ return b;
+ }
+
+ private static boolean subset(BitSet a, BitSet c) {
+ BitSet tmp = BitSet.newBitSet(a);
+ tmp.andNot(c);
+ return tmp.isEmpty();
+ }
+
+ /* ----------------------------------------------------------------------
+ * MAP tree
+ *
+ * The maximum over all of tree space is a DP over the subset lattice, so instead we run the DP
+ * over the observed-clade DAG using only the both-children-observed splits (classes 1 and 2 --
+ * exactly CCD0's split set) and then *certify* that no off-backbone tree can beat it.
+ *
+ * The certificate is a pair of upper-bound DPs over the same DAG. U(C) bounds the best subtree
+ * log-probability over ALL trees on C, and V(C) bounds it over trees that use at least one
+ * off-backbone (class 3 or 4) split. Any subtree contributes at most 0, so a novel child is
+ * bounded by 0; every class-3 split at C shares one theta, as does every class-4 split, because
+ * the model is uniform within a class. If best(root) > V(root), no tree using an off-backbone
+ * split anywhere can beat the backbone optimum, so the backbone MAP is the global MAP.
+ * ------------------------------------------------------------------- */
+
+ private volatile Map mapBest;
+ private volatile Map mapArg;
+ private volatile double offBackboneBound = Double.NaN;
+
+ /** All both-children-observed bipartitions of {@code cBits} (classes 1 and 2), each once. */
+ private List backboneSplits(BitSet cBits) {
+ int m = cBits.cardinality();
+ List out = new ArrayList<>();
+ for (BitSet d : sortedCladeBits()) {
+ if (d.cardinality() >= m || !subset(d, cBits)) {
+ continue;
+ }
+ BitSet complement = BitSet.newBitSet(cBits);
+ complement.andNot(d);
+ if (getClade(complement) == null || compareBitSets(d, complement) >= 0) {
+ continue;
+ }
+ out.add(new BitSet[]{d, complement});
+ }
+ return out;
+ }
+
+ /** Log theta shared by every split of the given off-backbone class at {@code cBits}. */
+ private double logThetaOfClass(BitSet cBits, int cls) {
+ double[] size = classSizes(cBits);
+ if (size[cls] <= 0) {
+ return Double.NEGATIVE_INFINITY;
+ }
+ double[] wz = logWeightsAndZ(cBits, alpha, alpha1, alpha2);
+ return wz[cls] - Math.log(wz[4]);
+ }
+
+ private synchronized void computeMAP() {
+ if (mapBest != null) {
+ return;
+ }
+ List clades = new ArrayList<>(getClades());
+ clades.sort(java.util.Comparator.comparingInt(Clade::size));
+
+ Map best = new HashMap<>();
+ Map arg = new HashMap<>();
+ Map upper = new HashMap<>(); // U: best over all trees
+ Map upperOff = new HashMap<>(); // V: best over trees using an off-backbone split
+
+ for (Clade c : clades) {
+ BitSet cb = c.getCladeInBits();
+ if (c.size() == 1) {
+ best.put(cb, 0.0);
+ upper.put(cb, 0.0);
+ upperOff.put(cb, Double.NEGATIVE_INFINITY);
+ continue;
+ }
+ double bBest = Double.NEGATIVE_INFINITY;
+ BitSet[] bArg = null;
+ double bUpper = Double.NEGATIVE_INFINITY;
+ double bOff = Double.NEGATIVE_INFINITY;
+
+ for (BitSet[] s : backboneSplits(cb)) {
+ Double l = best.get(s[0]);
+ Double r = best.get(s[1]);
+ if (l == null || r == null) {
+ continue;
+ }
+ double theta = logSplitProbability(cb, s[0], s[1], alpha, alpha1, alpha2);
+ double v = theta + l + r;
+ if (v > bBest) {
+ bBest = v;
+ bArg = s;
+ }
+ double ul = upper.get(s[0]);
+ double ur = upper.get(s[1]);
+ bUpper = Math.max(bUpper, theta + ul + ur);
+ double vl = upperOff.get(s[0]);
+ double vr = upperOff.get(s[1]);
+ bOff = Math.max(bOff, theta + Math.max(vl + ur, ul + vr));
+ }
+
+ // class 3: one child observed (bounded above by U of that child, novel side by 0)
+ double t3 = logThetaOfClass(cb, 2);
+ if (t3 > Double.NEGATIVE_INFINITY) {
+ double bestObservedSide = Double.NEGATIVE_INFINITY;
+ int m = cb.cardinality();
+ for (BitSet d : sortedCladeBits()) {
+ if (d.cardinality() >= m || !subset(d, cb)) {
+ continue;
+ }
+ BitSet complement = BitSet.newBitSet(cb);
+ complement.andNot(d);
+ if (getClade(complement) == null) { // exactly one side observed
+ Double u = upper.get(d);
+ if (u != null) {
+ bestObservedSide = Math.max(bestObservedSide, u);
+ }
+ }
+ }
+ if (bestObservedSide > Double.NEGATIVE_INFINITY) {
+ bUpper = Math.max(bUpper, t3 + bestObservedSide);
+ bOff = Math.max(bOff, t3 + bestObservedSide);
+ }
+ }
+
+ // class 4: both children novel, each bounded by 0
+ double t4 = logThetaOfClass(cb, 3);
+ if (t4 > Double.NEGATIVE_INFINITY) {
+ bUpper = Math.max(bUpper, t4);
+ bOff = Math.max(bOff, t4);
+ }
+
+ best.put(cb, bBest);
+ arg.put(cb, bArg);
+ upper.put(cb, bUpper);
+ upperOff.put(cb, bOff);
+ }
+
+ this.offBackboneBound = upperOff.get(getRootClade().getCladeInBits());
+ this.mapArg = arg;
+ this.mapBest = best;
+ }
+
+ /**
+ * Exact MAP over all trees that use no two-novel-clade split, by memoised recursion over
+ * classes {@code A_0} and {@code A_1}.
+ *
+ * Restricting to {@code A_0} keeps the recursion on the observed-clade DAG. Admitting
+ * {@code A_1} as well -- peel off an observed clade, leave a novel remainder -- widens the state
+ * space to clades of the form {@code root} minus a union of disjoint observed clades. That set
+ * can in principle be large, so the search is capped by {@link #MAP_STATE_BUDGET} distinct
+ * clades; in practice it stays small because an {@code A_1} split is expensive and the recursion
+ * only ever descends.
+ *
+ *
Returns {@code {best, viaA2}} for the clade: the best log probability using only
+ * {@code A_0}/{@code A_1} splits, and an upper bound on any subtree that uses an {@code A_2}
+ * split somewhere. The second is the certificate: if {@code best > viaA2} at the root, no tree
+ * containing a two-novel-clade split can reach the optimum, so the answer is the global MAP.
+ */
+ private static final long MAP_STATE_BUDGET =
+ Long.getLong("creg.mapStates", 4_000_000L);
+
+ private static final class BudgetExhausted extends RuntimeException {
+ BudgetExhausted() {
+ super(null, null, false, false);
+ }
+ }
+
+ private double[] solveFull(BitSet cBits, int a1Budget, List