Boundary membership
In windowed_statistics_fused, the per-variant scatter statistics assign each variant to a window with bin_idx = cp.searchsorted(we_gpu, positions), where we_gpu is the window end coordinates. This counts how many window ends lie strictly left of a position, so a variant whose position falls exactly on a window boundary (pos equal to a window end, which for contiguous windows is also the next window's start) is assigned to the lower window. Everything else in the same engine treats a window as the half-open interval [start, end): win_start / win_stop come from searchsorted(positions, [ws, we], side='left'), so n_variants = win_stop - win_start counts a boundary variant in the next window, and the dedicated scatter engines (_build_scatter_indices) use contains = pos < win_stop, also right-open. So a boundary-coincident variant lands in window w for the bin_idx-scattered stats but in window w+1 for n_variants and the theta / divergence stats.
Concrete example: positions 0, 100, ..., 11900 (120 sites), windows of 3000 bp with no overlap, so window ends are 3000, 6000, 9000, 12000. The site at pos == 3000 is reported in n_variants for the second window ([3000, 6000)), but bin_idx = searchsorted([3000, 6000, 9000, 12000], 3000) = 0 puts it in the first window's daf_hist / mu_sfs / mean_nsl. So those features for the first window include a variant that n_variants (and pi, theta, dxy) attribute to the second.
Overlapping windows
bin_idx = searchsorted(...) assigns each variant to exactly one window. When step_size < window_size a variant belongs to up to ceil(window_size / step_size) windows, and bin_idx drops it from all but one, so daf_hist / mu_sfs / mean_nsl undercount. The theta / divergence stats do not have this problem: they go through _compute_window_ranges, which builds n_per_var candidate windows per variant (its docstring: "scatter over (possibly overlapping) windows ... each variant falls inside up to n_per_var windows"). So within a single overlapping-window call, pi is correct while daf_hist / mu_sfs are wrong.
Measured: window_size=2000, step_size=1000 over 40 sites at pos 0, 100, ..., 3900 gives 3 of 4 windows whose daf_hist disagrees with diversity.daf_histogram over each window's [start, end) slice. It is silent, with no error or warning. It only errors if a non-fusable stat is co-requested, which reroutes to the WindowedAnalyzer and raises Unknown statistic, and that is incidental.
Affected statistics
The per-site bin_idx scatter path covers daf_hist, mu_sfs, and mean_nsl (via _windowed_mean). The broader scatter_stats set (snp_dist_*, mu_var, zns, omega, mu_ld, dist_var / dist_skew / dist_kurt) should be audited for the same single-window / boundary bin_idx pattern when this is picked up. GenotypeMatrix is unaffected; this is HaplotypeMatrix windowing.
Fix direction
Route the bin_idx scatter stats through the same window-membership machinery the theta / divergence stats use, _compute_window_ranges / _build_scatter_indices (n_per_var candidate windows per variant, right-open [start, end) containment), instead of the single-window searchsorted. That fixes both the boundary membership (right-open, matching n_variants) and the overlapping-window undercount in one place, and keeps all fused stats on one assignment.
If the full fix is deferred, a stopgap is to reject or warn on overlapping windows (step_size < window_size) for these stats, so they stop returning silently wrong values.
Validation
For every window, daf_hist / mu_sfs / mean_nsl should equal the scalar computed over that window's own [start, end) variant slice, checked across a multi-window grid (not just the whole-region single window the current parity tests use), and across both regimes:
- non-overlapping windows with a variant sitting exactly on an interior boundary (
pos equal to a window end, which is also the next window's start): it must land in the same window as n_variants;
- overlapping windows (
step_size < window_size): a variant in k windows must contribute to all k.
The removed multi-window test (test_multi_window_boundary_matches_scalar) covered the boundary case for non-overlapping windows and can be restored and extended when the fix lands.
Boundary membership
In
windowed_statistics_fused, the per-variant scatter statistics assign each variant to a window withbin_idx = cp.searchsorted(we_gpu, positions), wherewe_gpuis the window end coordinates. This counts how many window ends lie strictly left of a position, so a variant whose position falls exactly on a window boundary (posequal to a window end, which for contiguous windows is also the next window's start) is assigned to the lower window. Everything else in the same engine treats a window as the half-open interval[start, end):win_start/win_stopcome fromsearchsorted(positions, [ws, we], side='left'), son_variants = win_stop - win_startcounts a boundary variant in the next window, and the dedicated scatter engines (_build_scatter_indices) usecontains = pos < win_stop, also right-open. So a boundary-coincident variant lands in windowwfor thebin_idx-scattered stats but in windoww+1forn_variantsand the theta / divergence stats.Concrete example: positions 0, 100, ..., 11900 (120 sites), windows of 3000 bp with no overlap, so window ends are 3000, 6000, 9000, 12000. The site at
pos == 3000is reported inn_variantsfor the second window ([3000, 6000)), butbin_idx = searchsorted([3000, 6000, 9000, 12000], 3000) = 0puts it in the first window'sdaf_hist/mu_sfs/mean_nsl. So those features for the first window include a variant thatn_variants(andpi,theta,dxy) attribute to the second.Overlapping windows
bin_idx = searchsorted(...)assigns each variant to exactly one window. Whenstep_size < window_sizea variant belongs to up toceil(window_size / step_size)windows, andbin_idxdrops it from all but one, sodaf_hist/mu_sfs/mean_nslundercount. The theta / divergence stats do not have this problem: they go through_compute_window_ranges, which buildsn_per_varcandidate windows per variant (its docstring: "scatter over (possibly overlapping) windows ... each variant falls inside up ton_per_varwindows"). So within a single overlapping-window call,piis correct whiledaf_hist/mu_sfsare wrong.Measured:
window_size=2000,step_size=1000over 40 sites atpos0, 100, ..., 3900 gives 3 of 4 windows whosedaf_histdisagrees withdiversity.daf_histogramover each window's[start, end)slice. It is silent, with no error or warning. It only errors if a non-fusable stat is co-requested, which reroutes to theWindowedAnalyzerand raisesUnknown statistic, and that is incidental.Affected statistics
The per-site
bin_idxscatter path coversdaf_hist,mu_sfs, andmean_nsl(via_windowed_mean). The broaderscatter_statsset (snp_dist_*,mu_var,zns,omega,mu_ld,dist_var/dist_skew/dist_kurt) should be audited for the same single-window / boundarybin_idxpattern when this is picked up.GenotypeMatrixis unaffected; this isHaplotypeMatrixwindowing.Fix direction
Route the
bin_idxscatter stats through the same window-membership machinery the theta / divergence stats use,_compute_window_ranges/_build_scatter_indices(n_per_varcandidate windows per variant, right-open[start, end)containment), instead of the single-windowsearchsorted. That fixes both the boundary membership (right-open, matchingn_variants) and the overlapping-window undercount in one place, and keeps all fused stats on one assignment.If the full fix is deferred, a stopgap is to reject or warn on overlapping windows (
step_size < window_size) for these stats, so they stop returning silently wrong values.Validation
For every window,
daf_hist/mu_sfs/mean_nslshould equal the scalar computed over that window's own[start, end)variant slice, checked across a multi-window grid (not just the whole-region single window the current parity tests use), and across both regimes:posequal to a window end, which is also the next window's start): it must land in the same window asn_variants;step_size < window_size): a variant inkwindows must contribute to allk.The removed multi-window test (
test_multi_window_boundary_matches_scalar) covered the boundary case for non-overlapping windows and can be restored and extended when the fix lands.