Skip to content

Fix Euclidean sampling, shared sampler arguments, and input validation - #7

Open
bokveizen wants to merge 2 commits into
COMSOC-Community:mainfrom
bokveizen:fix/euclidean-independent-seeds
Open

bokveizen wants to merge 2 commits into
COMSOC-Community:mainfrom
bokveizen:fix/euclidean-independent-seeds

Conversation

@bokveizen

@bokveizen bokveizen commented Sep 30, 2026 •

Copy link
Copy Markdown

Fixes #6, reported by @masiarek.

Seeded Euclidean sampling currently draws voters and candidates from the same
stream, producing identical coordinates at equal population sizes. Gaussian-ball
sampling also repeats each accepted proposal, collapsing whole populations onto
one point. This PR repairs both paths and additional reproducible correctness
problems found while reviewing the samplers.

Changes

  • Derive separate voter and candidate integer seeds with SeedSequence.spawn(2).
    Give every ball-rejection proposal a fresh integer seed, including retries.
    Copy caller-owned position and inner-sampler dictionaries and validate every
    proposal's shape.
  • Use L2 distance for ordinal Euclidean sampling in every dimension. Normalize
    accepted flat 1D coordinate inputs to column arrays to prevent a crash.
  • Copy composition arguments separately for each sampler. Reusing one dictionary
    for groups of two and three voters now produces five voters, rather than six.
  • Preserve ordinal-truncation arguments and materialize per-voter approval-size
    iterables once, so generators work correctly.
  • Validate voter and candidate counts without assert, retaining validation under
    python -O; treat infinite counts as invalid integers.
  • Reject malformed/non-finite Gaussian scales and non-positive/non-finite
    truncation widths before entering rejection loops.

Reproducers

import numpy as np
from prefsampling.core.euclidean import EuclideanSpace, sample_election_positions
from prefsampling.ordinal import euclidean, identity
from prefsampling.core import concatenation

space = EuclideanSpace.UNIFORM_CUBE
voters, candidates = sample_election_positions(4, 4, 2, space, space, seed=42)
print(np.array_equal(voters, candidates))  # before: True; after: False

space = EuclideanSpace.GAUSSIAN_BALL
voters, candidates = sample_election_positions(6, 4, 2, space, space, seed=42)
print(len(np.unique(voters, axis=0)))  # before: 1; after: 6

print(euclidean(1, 2, 3, [[0, 0, 0]], [[1, 1, 1], [1.5, 0, 0]]))
# before: [[0, 1]]; after: [[1, 0]]

shared = {}
print(len(concatenation([2, 3], 2, [identity, identity], [shared, shared])))
# before: 6; after: 5; shared remains unchanged after the repair

Validation

  • Full unit-test suite on this PR branch: all 68 tests pass on Python 3.13.5
    with NumPy 2.5.3.
  • Source/test/validation lint passes with
    flake8 prefsampling tests validation --count --select=E9,F63,F7,F82 --show-source --statistics --jobs=2; the diff passes whitespace checks.
  • The nine RNG regression methods pass with NumPy 2.2.6 and 2.5.3. Against
    unmodified upstream they produce 43 failing subcases.
  • The same source and test changes passed all eight compatibility jobs in the
    fork CI run:
    Python 3.10–3.14 with compatible current NumPy, plus NumPy 1.21.6 on Python
    3.10, 1.26.4 on Python 3.12, and 2.2.6 on Python 3.13. This is fork validation;
    upstream PR CI status is separate. Python 3.7–3.9 have not been verified.

The regressions cover all six Euclidean spaces, equal/unequal population counts,
reproducibility, Gaussian-ball support and diversity, stable role streams,
explicit positions, shared dictionaries, custom sampler seeds, rejection paths
and fallback, 3D distances and tie classes, flat 1D coordinates, composition
counts, truncation generators, optimized-Python validation, and invalid Gaussian
parameters.

Compatibility

Public integer/None seed signatures remain unchanged. Seeded custom samplers
receive plain Python integer child seeds; unseeded election-position calls
continue to pass None. Top-level seed precedence is retained. An explicit inner
seed supplies the rejection sampler's root when the outer seed is None.

Exact historical seeded coordinates intentionally change. Corrected L2 distances
can change ordinal rankings and tie classes above two dimensions. Flat 1D
position values are preserved but their returned shape is normalized to (n, 1).
Copies of argument dictionaries are shallow.

Gaussian truncation widths must now be finite and strictly positive; scales must
be finite and non-negative. Very narrow positive bounds can still make sampling
slow. The existing ball-rejection cap, warning, and center-point fallback are
retained.

@bokveizen bokveizen changed the title Fix independent seeded streams in Euclidean position sampling and ball rejection Fix Euclidean sampling, shared sampler arguments, and input validation Sep 30, 2026

This branch has not been deployed

No deployments
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.

Seeded Euclidean sampling is degenerate: GAUSSIAN_BALL returns one point repeated, and candidates are drawn on top of voters

1 participant