Summary
diversity.pi (and the pi path through windowed_analysis) computes the
derived-allele count per variant as d = haplotypes.sum(axis=0). That works
when haplotype values are strictly {0, 1} (biallelic), but on a site whose
haplotype values include a 2 (triallelic under e.g. msprime's default
Jukes-Cantor mutation model), the int sum inflates d and the per-variant
pi falls back to a biallelic formula on incorrect counts.
Comparison to scikit-allel's allel.sequence_diversity shows ~1% per-window
discrepancy on data containing triallelic sites; on model="binary"
msprime data the two libraries agree to FP precision.
How to reproduce
import msprime
import allel
import cupy as cp
from pg_gpu import HaplotypeMatrix, diversity
# Default mutation model = jc69 -> can produce triallelic sites
ts = msprime.sim_ancestry(samples=20, sequence_length=100_000,
recombination_rate=1e-4, random_seed=42, ploidy=2)
ts = msprime.sim_mutations(ts, rate=1e-3, random_seed=42)
hm = HaplotypeMatrix.from_ts(ts)
# pg_gpu
pi_pg = diversity.pi(hm)
# scikit-allel
haps = cp.asnumpy(hm.haplotypes)
pos = cp.asnumpy(hm.positions)
ac = allel.HaplotypeArray(haps.T).count_alleles()
pi_a = allel.sequence_diversity(pos, ac)
print(f"pg_gpu: {pi_pg}, allel: {pi_a}, ratio: {pi_pg / pi_a}")
# observed: pg_gpu and allel disagree by ~1%
The same call with model="binary" produces matching values to FP precision.
What's actually wrong
pg_gpu/diversity.py:_site_contribution's 'pi' branch is:
return cp.where(seg, 2 * d * (n_safe - d) / (n_safe * (n_safe - 1)), 0.0)
This assumes d = count_of_derived_alleles and n_safe = total_haps. With
binary data, d = haps.sum(axis=0) is the derived-allele count. With
triallelic data, haps.sum(axis=0) mixes alt1 (+1) and alt2 (+2)
contributions and the resulting d no longer corresponds to a single
allele's count.
allel.mean_pairwise_difference(ac) handles the general case correctly
because ac carries one column per allele and the pairwise sum is over
all allele pairs.
Why it has been hiding
tests/test_scikit_allel_comparison.py constructs haplotype matrices by
hand with values restricted to {0, 1}, so count_alleles() returns
2-column ac and the formulas trivially agree. The discrepancy only
shows up against msprime data with the default JC mutation model, which
came up while building a test_streaming_vs_allel.py parity test for
streaming-from-zarr work.
Suggested fix
Replace the haps.sum(axis=0) DAC with a per-allele count from the
underlying (n_var, n_dip, 2) genotype block (or extend
HaplotypeMatrix to track per-allele counts directly). The pi formula
becomes a sum-over-allele-pairs the same way
allel.mean_pairwise_difference handles it. Same change applies to
other diversity / divergence statistics that derive a "derived-allele
count" from the int sum (theta_h, theta_l, fay_wu_h,
zeng_e, etc.).
Summary
diversity.pi(and thepipath throughwindowed_analysis) computes thederived-allele count per variant as
d = haplotypes.sum(axis=0). That workswhen haplotype values are strictly
{0, 1}(biallelic), but on a site whosehaplotype values include a
2(triallelic under e.g. msprime's defaultJukes-Cantor mutation model), the int sum inflates
dand the per-variantpi falls back to a biallelic formula on incorrect counts.
Comparison to scikit-allel's
allel.sequence_diversityshows ~1% per-windowdiscrepancy on data containing triallelic sites; on
model="binary"msprime data the two libraries agree to FP precision.
How to reproduce
The same call with
model="binary"produces matching values to FP precision.What's actually wrong
pg_gpu/diversity.py:_site_contribution's'pi'branch is:This assumes
d = count_of_derived_allelesandn_safe = total_haps. Withbinary data,
d = haps.sum(axis=0)is the derived-allele count. Withtriallelic data,
haps.sum(axis=0)mixes alt1 (+1) and alt2 (+2)contributions and the resulting
dno longer corresponds to a singleallele's count.
allel.mean_pairwise_difference(ac)handles the general case correctlybecause
accarries one column per allele and the pairwise sum is overall allele pairs.
Why it has been hiding
tests/test_scikit_allel_comparison.pyconstructs haplotype matrices byhand with values restricted to
{0, 1}, socount_alleles()returns2-column
acand the formulas trivially agree. The discrepancy onlyshows up against msprime data with the default JC mutation model, which
came up while building a
test_streaming_vs_allel.pyparity test forstreaming-from-zarr work.
Suggested fix
Replace the
haps.sum(axis=0)DAC with a per-allele count from theunderlying
(n_var, n_dip, 2)genotype block (or extendHaplotypeMatrixto track per-allele counts directly). The pi formulabecomes a sum-over-allele-pairs the same way
allel.mean_pairwise_differencehandles it. Same change applies toother diversity / divergence statistics that derive a "derived-allele
count" from the int sum (
theta_h,theta_l,fay_wu_h,zeng_e, etc.).