Skip to content

Widen RateStatistic domain to NonNegativeReal - #138

Merged
walterxie merged 1 commit into
RateStatisticfrom
fix/rate-statistic-domain
Aug 12, 2026
Merged

Widen RateStatistic domain to NonNegativeReal#138
walterxie merged 1 commit into
RateStatisticfrom
fix/rate-statistic-domain

Conversation

@alexeid

@alexeid alexeid commented Aug 12, 2026

Copy link
Copy Markdown
Member

Targets RateStatistic (#120) rather than master, so it can go in before that PR merges.

The problem

The new spec RateStatistic declares RealVector<PositiveReal>. It exposes three elements through that one domain — mean, variance, coefficient of variation — so the domain has to admit every value all three can take.

Variance and coefficient of variation are exactly 0 whenever every branch carries the same rate. That is not a corner case: it is the ordinary state of affairs under a strict clock. PositiveReal excludes 0 (lowerInclusive() returns false), so:

strict clock: mean=1.0 variance=0.0 cv=0.0
declared domain      : PositiveReal  (lower=0.0, lowerInclusive=false)
isValid(variance=0.0): false
isValid() over all 3 : false

Nothing in parser, core or inference calls isValid() today, so this is latent rather than a crash. But the spec migration plainly intends that enforcement, and by the time it arrives the class will have shipped in a release and been adopted — so it is much cheaper to correct the type parameter now than to change it under users later.

The change

PositiveRealNonNegativeReal, plus a comment on getDomain() recording why, so it does not get tightened back.

NonNegativeReal still rejects negatives, which none of the three statistics can be — the domain is widened by exactly the one value that was wrongly excluded, not loosened to plain Real.

For what it is worth, the sibling migrated class in the same package, beast.base.spec.evolution.Sum, declares RealScalar<Real> rather than a tighter positive domain, so declaring only what is actually guaranteed looks like the established convention.

Tests

Adds RateStatisticTest with four cases:

  • the strict-clock case — variance and cv are 0 and isValid() holds. This one fails against PositiveReal with "the declared domain must admit zero", so it pins the fix down.
  • negatives are still rejected, i.e. the domain was not weakened past what is meaningful.
  • weighted mean and unweighted variance for varying rates, checked against hand-computed values, which also documents that the mean is branch-length weighted while the variance is not.
  • the index guard on get(). Worth locking in: the legacy getArrayValue(dim) guarded with dim > 3, so dim == 3 fell through to an ArrayIndexOutOfBoundsException and negatives went unchecked. Rate statistic #120 quietly fixes that and the test records it.

beast-base suite on this branch: 462 tests, 0 failures.

The statistic exposes mean, variance and coefficient of variation through
one RealVector domain, so that domain has to admit every value the three
can take. Variance and coefficient of variation are exactly 0 whenever
the branches share a rate, which is the ordinary state of affairs under a
strict clock, and PositiveReal excludes 0 (lowerInclusive() is false):

  strict clock: mean=1.0 variance=0.0 cv=0.0
  isValid() over all 3 : false

Nothing in parser, core or inference calls isValid() yet, so this is
latent rather than a crash today -- but the spec migration plainly
intends that enforcement, and by then the class will have shipped and
been adopted.

NonNegativeReal still rejects negatives, which none of the three
statistics can be.

Adds RateStatisticTest: the strict-clock case (fails against
PositiveReal), that negatives are still rejected, the weighted-mean and
unweighted-variance values for varying rates, and the index guard on
get() that this migration tightened from the legacy `dim > 3` check.

beast-base suite: 462 tests, 0 failures.
@walterxie

Copy link
Copy Markdown
Member

I think we need to review the domain of all rates. see also #68.

@walterxie
walterxie merged commit 8336c38 into RateStatistic Aug 12, 2026
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