Repository: tskit-dev/msprime (language: python)

Issue:
Discrepancy between expected and observed coalescence time in beta coalescent for large values of alpha
Hey!

There appears to be an upward bias in the simulated coalescence times of the beta coalescent. This is relative to my expectation based on the time scaling provided in the documentation. The bias starts to be noticeable from `alpha=1.8` and increases substantially for values closer to 2.

Relative error between observed and expected coalescent times plotted against alpha:
![image](https://github.com/tskit-dev/msprime/assets/11058046/6f572525-befe-412a-988f-03eaf56d976a)

Code to generate figure:
```python
import multiprocessing as mp

import matplotlib.pyplot as plt
import msprime
import numpy as np
import scipy


def compute_beta_timescale(pop_size, alpha, ploidy):
    """
    Compute the generation time for the beta coalescent exactly as done in msprime when testing.
    https://github.com/tskit-dev/msprime/blob/804e0361c4f8b5f5051a9fbf411054ee8be3426a/verification.py#L3447
    """

    if ploidy > 1:
        N = pop_size / 2
        m = 2 + np.exp(
            alpha * np.log(2) + (1 - alpha) * np.log(3) - np.log(alpha - 1)
        )
    else:
        N = pop_size
        m = 1 + np.exp((1 - alpha) * np.log(2) - np.log(alpha - 1))
    ret = np.exp(
        alpha * np.log(m)
        + (alpha - 1) * np.log(N)
        - np.log(alpha)
        - scipy.special.betaln(2 - alpha, alpha)
    )
    return ret


def simulate_tree_height(alpha):
    """
    Simulate genealogies under the beta coalescent and compute the average tree height.
    Since we only have two lineages, the coalescence time should coincide with `compute_beta_timescale ()`.
    """

    # simulate genealogies under the beta coalescent
    g = msprime.sim_ancestry(
        samples=2,
        num_replicates=100000,
        model=msprime.BetaCoalescent(alpha=alpha),
        ploidy=1
    )

    return np.mean([ts.first().total_branch_length / 2 for ts in g])


if __name__ == "__main__":
    alphas = np.linspace(1.99, 1.999, 30)

    # simulate average tree heights in parallel
    with mp.Pool() as pool:
        heights_observed = np.array(pool.map(simulate_tree_height, alphas))

    # compute theoretical tree heights
    heights_theoretical = np.array([compute_beta_timescale(1, alpha, 1) for alpha in alphas])

    # compute relative difference between observed and theoretical
    diff_rel = (heights_observed - heights_theoretical) / heights_theoretical

    # plot relative difference against alpha
    plt.plot(alphas, diff_rel)
    plt.margins(0)
    plt.xlabel('alpha')
    plt.ylabel('diff_rel')
    plt.show()
```

Am I perhaps missing something?


Discussion / hints:
Thanks for the report @Sendrowski - the figure seems to have gone missing, would you mind posting again please?
Hm that’s interesting. Did it work this time?

![image](https://github.com/tskit-dev/msprime/assets/11058046/c1714e60-a252-471d-9965-137b185f9148)
No - maybe a problem with GitHub?
Possibly — I managed to view it with an independent device. [Here](https://private-user-images.githubusercontent.com/11058046/302304815-c1714e60-a252-471d-9965-137b185f9148.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDcxNDI3MzcsIm5iZiI6MTcwNzE0MjQzNywicGF0aCI6Ii8xMTA1ODA0Ni8zMDIzMDQ4MTUtYzE3MTRlNjAtYTI1Mi00NzFkLTk5NjUtMTM3YjE4NWY5MTQ4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMjA1VDE0MTM1N1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQ0MjY2NTBhZWEwMzQ4NTk0MDA3NGM4MTYxN2VjYjY5NDg4MDc4ZGJjOTA0NDNkMGEyNjhiMTAzMTU5OTlkYjMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.0bT0T7HiHoJpDP4hF7ivuWJkfvIiJ2Wa9mBFwxKESas) the link to the image if that is of any help.
Sorry, still not showing up for me (I got a 404 on that link)
And [here](https://www.icloud.com/iclouddrive/01atEZs77JcEu48TqTDkTlhyQ#IMG_8404) is an iCloud link. Hope it works :)
![Screenshot from 2024-02-05 15-08-50](https://github.com/tskit-dev/msprime/assets/2664569/b3241e6e-58e9-4a6a-8513-909a076b94f7)

Any thoughts here @JereKoskela ?
I ran a C++ script simulating Beta(2-a, a) random variables for a very close to 2, and relative errors of 5 or 6 per cent in an empirical mean from 10k samples are common because the true mean is so small. I think that explains what is going on below alpha = 1.995 or so.

As for the spike very close to 2, the timescale of the Schweinsberg Beta-coalescent model is unpleasant. It goes to infinity at alpha = 1 and zero at alpha = 2, so I wouldn't be surprised if numerical issues creep in near those boundaries. At the alpha = 2 boundary in particular, you're essentially sampling 0/0. The practical thing to do is just use the Hudson model, since probability of seeing a multiple merger in the the Beta-coalescent is effectively zero in that regime.
I agree that a relative error of this magnitude might perhaps not seem so surprising (100k replicates in my code example), but it is also important to note that the simulated values are consistently larger than the theoretical ones (I am not taking absolute values). For `alpha=1.9`, I obtain a relative error of ~2.7%  (1e6 replicates, very stable estimate), and for this value we have a (not so extreme) time scaling of about 0.14 relative to the Kingman coalescent. This plot is probably doing a better job ([iCloud link in case you still can’t see my images](https://www.icloud.com/iclouddrive/0ef4r7KSB24vjT0icW7viuu7A#beta_bias)):

![image](https://github.com/tskit-dev/msprime/assets/11058046/124b1fe2-4b47-40bd-9a90-542c9de50882)


You're right @Sendrowski, I missed the lack of a modulus sign, and evidently also one zero on the number of replicates. I'll do some more digging. It looks like a very odd bug; the code just calculates the timescale and multiplies it by an Exp(1) random variable. And somehow the resulting mean does not equal the timescale.
Yes, very odd indeed... The bias also appears to drop somewhat near `alpha=1.99` before shooting up again. I hope it won't be very hard to find out. I didn't debug the C code, so perhaps the time scaling is simply different when calculated there. Or perhaps it is caused by including the incomplete beta function although I could not observe that when translating into native Python.
Ok, this turned out to be an insufficiently sensitive polynomial approximation for evaluating (essentially) `x^2 / x^2` for very small `x`. It was using a polynomial approximation for `x < 1e-9`, which I've now hiked to `x < 1e-5`. @Sendrowski, your Python script now yields unbiased estimators with errors on both sides of zero.

@jeromekelleher, any comments or are you happy to merge the linked PR? I've rerun the tests in verification.py for the Beta-coalescent and all still look good.
Thanks for sorting this out so quickly @JereKoskela! Change LGTM.

@Sendrowski, are you happy with the changes? Shall we push out a bugfix release?
Also looks good to me! Thank you for the quick fix @JereKoskela, and I look forward to meeting you in Warwick in April. As for the release, it’s totally up to you how soon you would like to push it out.
Hey again. The bias due to numerical imprecision for values of alpha very close to 2 persists, however. Maybe one should prohibit values greater than 1.99 or so to make sure this won't be causing any problems.

![image](https://github.com/tskit-dev/msprime/assets/11058046/e23c29d8-39c2-43f9-989c-2fb1eb1adebb)

Sorry, still can't see the image @Sendrowski . I think maybe you're uploading a tiff instead of a png or something?
Hm it should be a png, but the graph I enclosed actually looks identical to the one I [originally posted](https://www.icloud.com/iclouddrive/01atEZs77JcEu48TqTDkTlhyQ#IMG_8404).

![msprime_bug](https://github.com/tskit-dev/msprime/assets/11058046/d8a4463c-ca9d-4163-8ba2-ebc47e963e3e)

Yeah, the timescale will always break close to alpha = 1 or 2. There is no way to avoid that. I suppose we could disallow values very close to the boundaries as a result. I'd like to make sure that 1.01 and 1.99 work even if they come from floating point calculations in some other script, so maybe 1.009 and 1.991?
Sounds good! Of note perhaps that I could not observe the same kind of numerical instability for values near 1 — at least for similar distances to the boundary.
Thanks, that's useful to know. It's possible that we can get much closer to 1 than 2, but eventually everything will be infinite for alpha too close to 1. I'll see if I can find a reasonable lower bound. 
Shall we reopen this issue, or make a new one?
I've reopened this one. I'm hoping to get to this within a few days.

Fix patch:
diff --git a/docs/ancestry.md b/docs/ancestry.md
index 06a29974f..dcf2d65a1 100644
--- a/docs/ancestry.md
+++ b/docs/ancestry.md
@@ -2381,6 +2381,13 @@ tree = ts.first()
 print(tree.tmrca(0,1))
 
 ```
+:::{important}
+Both the haploid and polyploid timescales collapse to zero as {math}`\alpha`
+approaches 2. For reasons of numerical stability, values above
+{math}`\alpha = 1.991` have been prohibited. The
+{ref}`Hudson coalescent<sec_ancestry_models_hudson>` provides a very good
+approximation for {math}`1.991 <= \alpha <= 2`.
+:::
 
 The Dirac-coalescent is simulated similarly in both the diploid case:
 
diff --git a/lib/msprime.c b/lib/msprime.c
index 3ad619a89..474f7ca7e 100644
--- a/lib/msprime.c
+++ b/lib/msprime.c
@@ -7416,7 +7416,7 @@ msp_set_simulation_model_beta(msp_t *self, double alpha, double truncation_point
 {
     int ret = 0;
 
-    if (alpha <= 1.0 || alpha >= 2.0) {
+    if (alpha <= 1.0 || alpha > 1.991) {
         ret = MSP_ERR_BAD_BETA_MODEL_ALPHA;
         goto out;
     }
diff --git a/lib/util.c b/lib/util.c
index 6f244bfca..6b8342d13 100644
--- a/lib/util.c
+++ b/lib/util.c
@@ -194,7 +194,7 @@ msp_strerror_internal(int err)
             ret = "Proportion values must have 0 <= x <= 1";
             break;
         case MSP_ERR_BAD_BETA_MODEL_ALPHA:
-            ret = "Bad alpha. Must have 1 < alpha < 2";
+            ret = "Bad alpha. Must have 1 < alpha <= 1.991";
             break;
         case MSP_ERR_BAD_TRUNCATION_POINT:
             ret = "Bad truncation_point. Must have 0 < truncation_point.";
@@ -682,7 +682,7 @@ fast_search_alloc(fast_search_t *self, const double *elements, size_t n_elements
      * of the array. The rest of the lookup element indexes point to (max_element *
      * query_multiplier) non-zero element values.
      */
-    self->num_lookups = 2 + (size_t)(max_element * self->query_multiplier);
+    self->num_lookups = 2 + (size_t) (max_element * self->query_multiplier);
 
     self->query_cutoff = ((double) self->num_lookups - 1) / self->query_multiplier;
 


Test patch:
diff --git a/lib/tests/test_ancestry.c b/lib/tests/test_ancestry.c
index 7ca4183a8..9974758a1 100644
--- a/lib/tests/test_ancestry.c
+++ b/lib/tests/test_ancestry.c
@@ -1590,7 +1590,7 @@ test_beta_coalescent_bad_parameters(void)
     int ret;
     msp_t msp;
     unsigned int n = 10;
-    double alphas[] = { -1e6, 0, 0.001, 1.0, 2.0, 1e6 };
+    double alphas[] = { -1e6, 0, 0.001, 1.0, 1.995, 1e6 };
     double truncation_points[] = { -1e6, 0 };
     gsl_rng *rng = safe_rng_alloc();
     tsk_table_collection_t tables;