prefsampling 0.1.24 · numpy 2.2.6 · pref_voting 1.18.1 spaces: uniform_ball, uniform_sphere, uniform_cube, gaussian_ball, gaussian_cube, unbounded_gaussian ============================================================================== 1. GAUSSIAN_BALL, seeded: how many DISTINCT points come back? ============================================================================== euclidean_space_to_sampler pins the seed inside inner_sampler_args, and ball_resampling's outer loop draws every point from that same dict. seed points distinct first point 0 8 1 [+0.041491 -0.043595] 1 8 1 [+0.114043 +0.271134] 7 8 1 [+0.000406 +0.098586] 42 8 1 [+0.100557 -0.343195] 2026 8 1 [-0.261730 +0.079389] Expected if correct: 8 distinct. Observed: 1, for every seed. UNSEEDED (seed=None) is fine — the defect is created BY seeding, which is the practice the package recommends for reproducibility: distinct points, seed=None: 8 of 8 ============================================================================== 2. Does the rejection branch 'almost never' fire? (No — about a third.) ============================================================================== first draw lands OUTSIDE the ball: 625/2000 = 31.2% analytic (Rayleigh tail, sigma=0.33, r=0.5, d=2): exp(-r^2/2s^2) = 31.7% So an early draft's 'the branch effectively never runs' was wrong. It does not matter: the defect is the seed PERSISTING in inner_sampler_args, so once any point is accepted, every later outer iteration re-draws that same point. Identical points with probability 1, not 'usually'. ============================================================================== 3. sample_election_positions: does candidate j land on voter j? ============================================================================== Both _sample_points calls receive the same `seed`, and _sample_points does positions_args['seed'] = seed AFTER merging user args — so a caller cannot even work around it by passing per-side seeds. 5 voters x 3 candidates (a 'hit' = candidate j at exactly voter j; 3 comparable indices) uniform_ball every index collides in 0/200 seeds clean uniform_sphere every index collides in 0/200 seeds clean uniform_cube every index collides in 200/200 seeds COLLIDES gaussian_ball every index collides in 200/200 seeds COLLIDES gaussian_cube every index collides in 200/200 seeds COLLIDES unbounded_gaussian every index collides in 200/200 seeds COLLIDES 4 voters x 4 candidates (a 'hit' = candidate j at exactly voter j; 4 comparable indices) uniform_ball every index collides in 200/200 seeds COLLIDES uniform_sphere every index collides in 200/200 seeds COLLIDES uniform_cube every index collides in 200/200 seeds COLLIDES gaussian_ball every index collides in 200/200 seeds COLLIDES gaussian_cube every index collides in 200/200 seeds COLLIDES unbounded_gaussian every index collides in 200/200 seeds COLLIDES Equal counts, other sizes and dimensions — the candidate array IS the voter array (same function, same arguments): d=1: identical for all 6 spaces x (3,6,10) x 20 seeds: True d=2: identical for all 6 spaces x (3,6,10) x 20 seeds: True d=3: identical for all 6 spaces x (3,6,10) x 20 seeds: True control — DIFFERENT spaces both sides (ball vs cube, 4x4): identical: False The escape at unequal counts is NOT protective and NOT rejection sampling: ball_uniform is the direct polar method with no rejection at all. It draws rng.normal(size=(num_dimensions, num_points)) — a DIMENSION-major array — so the mapping from stream position to (point, coordinate) depends on the point count. Change the count and the correspondence scrambles; make the counts equal and it lines back up. That is an accident of array shape. ============================================================================== 4. Why the shape decides it: same seed, 5 points vs 3 points ============================================================================== cube (point-major) point j identical in both runs: 3/3 gaussian (point-major) point j identical in both runs: 3/3 gaussian + widths (loop) point j identical in both runs: 3/3 ball_uniform (dim-major) point j identical in both runs: 0/3 cube does rng.random((num_points, num_dimensions)) -> C-order, row per point, so point j owns stream slots [j*d, j*d+d) whatever n is. gaussian without widths fills (num_points, num_dimensions) the same way; with widths it draws d normals per point in a rejection loop, and identical seeds walk identical accept/reject paths — n-independent too. ball_uniform does rng.normal(size=(num_dimensions, num_points)) -> the first n draws are the x-coordinate of every point. n changes, the whole correspondence shifts. And the escape is not independence — at 5 voters x 3 candidates on uniform_ball, voter j and candidate j share the raw draw behind their x numerator: corr(voter0.x, cand0.x) over 2000 seeds: +0.81 corr(voter0.y, cand0.y): -0.03 mean |voter0 - cand0| same seed: 0.332 independent draws: 0.454 ============================================================================== 5. Downstream — pref_voting.generate_profile(probmodel='euclidean') ============================================================================== space distinct profiles over 300 seeds (5 voters, 3 candidates) uniform_ball 277 uniform_sphere 265 uniform_cube 127 gaussian_ball 1 gaussian_cube 126 unbounded_gaussian 114 gaussian_ball returns ONE profile for every seed: all voters collapse to a single point AND the candidates collapse onto that same point, so every distance is zero, every ballot is a total tie, and the ranking that comes back is index order. That is the shape a Condorcet-cycle sweep reads as a suspiciously clean 0.00%. The other five look healthy at a glance on THIS count — 5 != 3 — which is exactly why the duplication has to be checked separately (section 3), and why filing 'pref_voting users are affected' flatly would have been refuted in one line by a maintainer testing the default path. Baseline check for the two 'escape' spaces. Comparing the ONE seeded figure above against unseeded reruns would be comparing a point to a distribution, so both sides are sampled: 10 independent SEEDED blocks of 300 consecutive seeds each against 10 UNSEEDED repetitions. space seeded mean(sd) unseeded mean(sd) shift ranges uniform_ball 278.9 (3.6) 285.1 (5.4) +6.2 OVERLAP: [274-285] vs [279-295] uniform_sphere 269.0 (2.6) 286.2 (3.7) +17.2 disjoint: [265-272] vs [279-293] Seeding shifts the whole distribution down even on the spaces where no point actually collides -- the footprint of the voter/candidate entanglement measured in section 4. Seeding degrades all six, not four. Read the two rows differently, though. uniform_sphere's seeded and unseeded ranges do not overlap at all, which needs no statistics. uniform_ball's DO overlap: the shift is real but roughly two pooled standard deviations, so it is a claim about distributions that cannot be made by comparing single runs. An earlier draft of this probe compared one seeded block against three unseeded reps and concluded that every unseeded rep beats the seeded figure. That held for the samples it drew and is not true in general -- with ten blocks a side, seeded uniform_ball reaches the mid-280s and unseeded dips to 280. NOTE: this block is the ONE non-reproducible measurement in the probe -- half of it is unseeded by definition, so these numbers shift between runs and probe.out will not diff clean here. What reproduces is the comparison: a downward shift on both spaces, disjoint on uniform_sphere and overlapping on uniform_ball. Everything else in this file is deterministic.