diff --git a/src/main/java/ccd/algorithms/credibleSets/CredibleCCDComputer.java b/src/main/java/ccd/algorithms/credibleSets/CredibleCCDComputer.java index 293b07b..32c5f71 100644 --- a/src/main/java/ccd/algorithms/credibleSets/CredibleCCDComputer.java +++ b/src/main/java/ccd/algorithms/credibleSets/CredibleCCDComputer.java @@ -370,18 +370,9 @@ private void writeCurrentResult(double remainingProbability) { } } - public static double logBigInteger(BigInteger val) { - int precision = Math.max((int) (Math.log(val.bitLength()) / Math.log(2)), 20); // Ensure sufficient precision - BigDecimal bigDecimalVal = new BigDecimal(val); - int scale = bigDecimalVal.scale(); - - // Scale value for improved precision - BigDecimal scaledValue = bigDecimalVal.movePointLeft(scale); - - // Compute the logarithm using BigDecimal - double log2 = Math.log(scaledValue.doubleValue()); - - // Adjust the logarithm based on the scale - return log2 + scale * Math.log(10); - } + // NOTE: a logBigInteger implementation lived here and was removed. It was incorrect: + // new BigDecimal(aBigInteger).scale() is always 0, so movePointLeft(scale) was a no-op and + // the method reduced to Math.log(val.doubleValue()), which is infinite above ~1.8e308 -- + // i.e. for essentially every tree count this class deals with. + // Use AbstractCCD.logBigInteger(BigInteger) instead. */ diff --git a/src/main/java/ccd/model/AbstractCCD.java b/src/main/java/ccd/model/AbstractCCD.java index 73a8bce..fd5876b 100644 --- a/src/main/java/ccd/model/AbstractCCD.java +++ b/src/main/java/ccd/model/AbstractCCD.java @@ -755,6 +755,13 @@ public double getEntropy() { return -testro; } + /** + * {@inheritDoc} + * + *
The default implementation returns the number of topologies represented by the CCD + * graph, which is the support for any model that assigns probability only within its graph. + * Full-support subclasses must override this; see {@link ITreeDistribution#getNumberOfTrees()}. + */ @Override public BigInteger getNumberOfTrees() { if (numberOfTopologiesDirty) { @@ -764,6 +771,48 @@ public BigInteger getNumberOfTrees() { return this.rootClade.getNumberOfTopologies(); } + /** + * The number of rooted binary topologies on {@code n} labelled taxa, {@code (2n-3)!!}. + * Returns {@code 1} for {@code n <= 2}. This is the support size of any full-support model + * on {@code n} taxa. + * + * @param n number of taxa + * @return {@code (2n-3)!!} as a {@link BigInteger} + */ + public static BigInteger numberOfRootedTopologies(int n) { + BigInteger result = BigInteger.ONE; + for (int k = 2 * n - 3; k > 1; k -= 2) { + result = result.multiply(BigInteger.valueOf(k)); + } + return result; + } + + /** + * Natural logarithm of a positive {@link BigInteger}, correct for values far outside + * {@code double} range. + * + *
{@code Math.log(value.doubleValue())} is not usable here: a {@code double} overflows to + * infinity above about 1.8e308, i.e. beyond roughly 1024 bits, and tree counts in this + * package routinely exceed that. Instead the value is shifted right so that at most 1000 + * bits remain, comfortably inside {@code double} range, and the shift is added back as + * {@code shift * log 2}. Precision is unaffected, since {@code double} carries only 53 + * mantissa bits either way. + * + * @param value a strictly positive value + * @return the natural logarithm of {@code value} + * @throws IllegalArgumentException if {@code value} is not positive + */ + public static double logBigInteger(BigInteger value) { + if (value.signum() <= 0) { + throw new IllegalArgumentException("log of non-positive BigInteger: " + value); + } + int shift = value.bitLength() - 1000; + if (shift > 0) { + return Math.log(value.shiftRight(shift).doubleValue()) + shift * Math.log(2.0); + } + return Math.log(value.doubleValue()); + } + /** * Returns the AIC score of this CCD. * The number of parameters depends on the specific CCD. diff --git a/src/main/java/ccd/model/ITreeDistribution.java b/src/main/java/ccd/model/ITreeDistribution.java index 3bdb59d..2cff739 100644 --- a/src/main/java/ccd/model/ITreeDistribution.java +++ b/src/main/java/ccd/model/ITreeDistribution.java @@ -72,7 +72,25 @@ public interface ITreeDistribution { */ public boolean containsTree(Tree tree); - /** @return the number of trees (topologies) in this distribution */ + /** + * Returns the size of this distribution's support, that is, the number of distinct + * tree topologies to which it assigns non-zero probability. + * + *
For a CCD whose support is exactly the set of topologies its graph represents (CCD0, + * CCD1, regCCD) this equals the number of topologies of the graph. For a full-support model + * (KRegCCD, MRegCCD, {@link UniformEscapeCCD}) it is the number of rooted topologies on the + * taxon set, which is strictly larger: those models place probability outside their graph. + * Implementations must report the support, not the graph, so that + * {@code getNumberOfTrees()} and {@link #containsTree(Tree)} agree. + * + *
The result is a {@link BigInteger} because it overflows {@code long} at a handful of + * taxa and {@code double} not long after: the number of rooted topologies needs 840 bits on + * 129 taxa and 1093 bits on 160, and a {@code double} overflows past about 1024 bits. Take + * logarithms with {@link AbstractCCD#logBigInteger(BigInteger)}, not via + * {@code doubleValue()}, which is infinite from roughly 155 taxa upwards. + * + * @return the number of topologies with non-zero probability under this distribution + */ public BigInteger getNumberOfTrees(); /** diff --git a/src/main/java/ccd/model/KRegCCD.java b/src/main/java/ccd/model/KRegCCD.java index 0018769..ad66285 100644 --- a/src/main/java/ccd/model/KRegCCD.java +++ b/src/main/java/ccd/model/KRegCCD.java @@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.List; import java.util.Map; +import java.math.BigInteger; /** * Remco's "blue-region" CCD regularisation with full support (every tree @@ -645,6 +646,18 @@ public boolean containsTree(Tree tree) { return true; } + /** + * {@inheritDoc} + * + *
KRegCCD is full support, so its support is every rooted topology on the taxon set, not just + * the topologies of its graph. The inherited graph count would understate this by many orders + * of magnitude and would contradict {@link #containsTree(Tree)}, which is always {@code true}. + */ + @Override + public BigInteger getNumberOfTrees() { + return numberOfRootedTopologies(getNumberOfLeaves()); + } + /** * Number of internal clades of {@code tree} that are not present in this CCD's observed clade * set (the novel-clade count = the total {@code m-2} over the tree's blue regions). This is the diff --git a/src/main/java/ccd/model/MRegCCD.java b/src/main/java/ccd/model/MRegCCD.java index 2f11b7a..7d68761 100644 --- a/src/main/java/ccd/model/MRegCCD.java +++ b/src/main/java/ccd/model/MRegCCD.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.math.BigInteger; /** * MRegCCD -- the one-parameter "per-new-split" regularised CCD. It unifies RegCCD's split-expansion @@ -182,6 +183,18 @@ public boolean containsTree(Tree tree) { return true; } + /** + * {@inheritDoc} + * + *
MRegCCD is full support, so its support is every rooted topology on the taxon set, not just
+ * the topologies of its graph. The inherited graph count would understate this by many orders
+ * of magnitude and would contradict {@link #containsTree(Tree)}, which is always {@code true}.
+ */
+ @Override
+ public BigInteger getNumberOfTrees() {
+ return numberOfRootedTopologies(getNumberOfLeaves());
+ }
+
private double scoreTree(Tree tree, double scoreMu) {
Map Uses the same four-taxon worked example as {@link UniformEscapeCCDTest}: two sampled trees,
+ * a backbone covering only those two, and 15 rooted topologies in total. A full-support model
+ * must therefore report 15, not 2 -- the failure mode this test exists to catch is a full-support
+ * model inheriting the graph-based count from {@link AbstractCCD}.
+ */
+public class NumberOfTreesContractTest {
+
+ private static final List