Skip to content

Commit

Permalink
Euclidean updated
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon-Rey committed Mar 8, 2024
1 parent 29261d7 commit dd8c858
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 160 deletions.
3 changes: 1 addition & 2 deletions prefsampling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
from itertools import chain

from prefsampling.approval import NoiseType
from prefsampling.core.euclidean import EuclideanSpace
from prefsampling.ordinal import TreeSampler


class CONSTANTS(Enum):
""" All constants of the package """
_ignore_ = 'member cls'
cls = vars()
for member in chain(list(EuclideanSpace), list(TreeSampler), list(NoiseType)):
for member in chain(list(TreeSampler), list(NoiseType)):
if member.name in cls:
raise ValueError(f"The name {member.name} is used in more than one enumeration. The"
f"CONSTANTS class needs unique names to be well-defined.")
Expand Down
49 changes: 34 additions & 15 deletions prefsampling/approval/euclidean.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
from __future__ import annotations

from collections.abc import Callable

import numpy as np

from prefsampling.core.euclidean import election_positions, EuclideanSpace
from prefsampling.core.euclidean import sample_election_positions
from prefsampling.inputvalidators import validate_num_voters_candidates


@validate_num_voters_candidates
def euclidean(
num_voters: int,
num_candidates: int,
point_sampler: Callable,
point_sampler_args: dict,
candidate_point_sampler: Callable = None,
candidate_point_sampler_args: dict = None,
radius: float = 0.5,
space: EuclideanSpace = EuclideanSpace.UNIFORM,
dimension: int = 2,
seed: int = None,
) -> list[set[int]]:
"""
Expand All @@ -35,29 +39,44 @@ def euclidean(
Number of Voters.
num_candidates : int
Number of Candidates.
point_sampler : Callable
The sampler used to sample point in the space. Used for both voters and candidates
unless a `candidate_space` is provided.
point_sampler_args : dict
The arguments passed to the `point_sampler`. The argument `num_points` is ignored
and replaced by the number of voters or candidates.
candidate_point_sampler : Callable, default: :code:`None`
The sampler used to sample the points of the candidates. If a value is provided,
then the `space` argument is only used for voters.
candidate_point_sampler_args : dict
The arguments passed to the `candidate_point_sampler`. The argument `num_points`
is ignored and replaced by the number of candidates.
radius : float, default: 0.5
Radius of approval.
space : EuclideanSpace, default: :py:const:`~prefsampling.core.euclidean.EuclideanSpace.UNIFORM`
Type of space considered. Should be a constant defined in the
:py:class:`~prefsampling.core.euclidean.EuclideanSpace`.
dimension : int, default: 2
Number of Dimensions.
seed : int
Seed for numpy random number generator.
seed : int, default: :code:`None`
Seed for numpy random number generator. Also passed to the point samplers if
a value is provided.
Returns
-------
list[set[int]]
Approval votes.
"""
rng = np.random.default_rng(seed)
votes = [set() for _ in range(num_voters)]
voters, candidates = election_positions(
num_voters, num_candidates, space, dimension, rng

voters_pos, candidates_pos = sample_election_positions(
num_voters,
num_candidates,
point_sampler,
point_sampler_args,
candidate_point_sampler,
candidate_point_sampler_args,
seed,
)

votes = [set() for _ in range(num_voters)]
for v in range(num_voters):
for c in range(num_candidates):
if radius >= np.linalg.norm(voters[v] - candidates[c]):
if radius >= np.linalg.norm(voters_pos[v] - candidates_pos[c]):
votes[v].add(c)

return votes
143 changes: 52 additions & 91 deletions prefsampling/core/euclidean.py
Original file line number Diff line number Diff line change
@@ -1,112 +1,73 @@
from __future__ import annotations

from enum import Enum
from collections.abc import Callable

import numpy as np

from prefsampling.inputvalidators import validate_num_voters_candidates


class EuclideanSpace(Enum):
"""
Constants used to represent Euclidean spaces
"""

UNIFORM = "Uniform Space"
"""
Uniform space
"""
GAUSSIAN = "Gaussian Space"
"""
Gaussian space
"""
SPHERE = "Spherical Space"
"""
Spherical space
"""
BALL = "Ball Space"
"""
Ball-shaped space
"""


@validate_num_voters_candidates
def election_positions(
def sample_election_positions(
num_voters: int,
num_candidates: int,
space: EuclideanSpace,
dimension: int,
rng: np.random.Generator,
) -> (np.ndarray, np.ndarray):
point_sampler: Callable,
point_sampler_args: dict,
candidate_point_sampler: Callable = None,
candidate_point_sampler_args: dict = None,
seed: int = None,
) -> tuple[np.ndarray, np.ndarray]:
"""
Returns the position of the voters and the candidates in a Euclidean space.
Parameters
----------
num_voters: int
The number of voters.
num_candidates: int
The number of candidates.
space : :py:class:`~prefsampling.core.euclidean.EuclideanSpace`
Type of space considered. Should be a constant defined in the
:py:class:`~prefsampling.core.euclidean.EuclideanSpace`.
dimension : int
Number of dimensions for the space considered.
rng : np.random.Generator
The numpy generator to use for randomness.
num_voters : int
Number of Voters.
num_candidates : int
Number of Candidates.
point_sampler : Callable
The sampler used to sample point in the space. Used for both voters and candidates
unless a `candidate_space` is provided.
point_sampler_args : dict
The arguments passed to the `point_sampler`. The argument `num_points` is ignored
and replaced by the number of voters or candidates.
candidate_point_sampler : Callable, default: :code:`None`
The sampler used to sample the points of the candidates. If a value is provided,
then the `space` argument is only used for voters.
candidate_point_sampler_args : dict
The arguments passed to the `candidate_point_sampler`. The argument `num_points`
is ignored and replaced by the number of candidates.
seed : int, default: :code:`None`
Seed for numpy random number generator. Also passed to the point samplers if
a value is provided.
Returns
-------
(np.ndarray, np.ndarray)
The position of the voters and of the candidates respectively.
"""
if isinstance(space, Enum):
space = EuclideanSpace(space.value)
else:
space = EuclideanSpace(space)
if space == EuclideanSpace.UNIFORM:
voters = rng.random((num_voters, dimension))
candidates = rng.random((num_candidates, dimension))
elif space == EuclideanSpace.GAUSSIAN:
voters = rng.normal(loc=0.5, scale=0.15, size=(num_voters, dimension))
candidates = rng.normal(loc=0.5, scale=0.15, size=(num_candidates, dimension))
elif space == EuclideanSpace.SPHERE:
voters = np.array(
[list(random_sphere(dimension, rng)[0]) for _ in range(num_voters)]
)
candidates = np.array(
[list(random_sphere(dimension, rng)[0]) for _ in range(num_candidates)]
)
elif space == EuclideanSpace.BALL:
voters = np.array(
[list(random_ball(dimension, rng)[0]) for _ in range(num_voters)]
)
candidates = np.array(
[list(random_ball(dimension, rng)[0]) for _ in range(num_candidates)]
)
else:
raise ValueError(
"The `space` argument needs to be one of the constant defined in the "
"core.euclidean.EuclideanSpace enumeration. Choices are: "
+ ", ".join(str(s) for s in EuclideanSpace)
)
return voters, candidates

tuple[np.ndarray, np.ndarray]
The positions of the voters and of the candidates.
def random_ball(
dimension: int, rng: np.random.Generator, num_points: int = 1, radius: float = 1
) -> np.ndarray:
random_directions = rng.normal(size=(dimension, num_points))
random_directions /= np.linalg.norm(random_directions, axis=0)
random_radii = rng.random(num_points) ** (1 / dimension)
x = radius * (random_directions * random_radii).T
return x
"""
if candidate_point_sampler is not None and candidate_point_sampler_args is None:
raise ValueError("If candidate_point_sampler is not None, a value needs to be "
"passed to candidate_point_sampler_args (even if it's just "
"an empty dictionary).")

if seed is not None:
point_sampler_args["seed"] = seed
if candidate_point_sampler is not None:
candidate_point_sampler_args["seed"] = seed

def random_sphere(
dimension: int, rng: np.random.Generator, num_points: int = 1, radius: float = 1
) -> np.ndarray:
random_directions = rng.normal(size=(dimension, num_points))
random_directions /= np.linalg.norm(random_directions, axis=0)
random_radii = 1.0
return radius * (random_directions * random_radii).T
point_sampler_args['num_points'] = num_voters
voters_pos = point_sampler(**point_sampler_args)
dimension = len(voters_pos[0])
if candidate_point_sampler is None:
point_sampler_args['num_points'] = num_candidates
candidates_pos = point_sampler(**point_sampler_args)
else:
candidate_point_sampler_args['num_points'] = num_candidates
candidates_pos = candidate_point_sampler(**candidate_point_sampler_args)
if len(candidates_pos[0]) != dimension:
raise ValueError("The position of the voters and of the candidates do not have the "
"same dimension. Use different point samplers to solve this "
"problem.")
return voters_pos, candidates_pos
49 changes: 34 additions & 15 deletions prefsampling/ordinal/euclidean.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
from __future__ import annotations

from collections.abc import Callable

import numpy as np
from numpy import linalg

from prefsampling.core.euclidean import election_positions, EuclideanSpace
from prefsampling.core.euclidean import sample_election_positions
from prefsampling.inputvalidators import validate_num_voters_candidates


@validate_num_voters_candidates
def euclidean(
num_voters: int,
num_candidates: int,
space: EuclideanSpace = EuclideanSpace.UNIFORM,
dimension: int = 2,
point_sampler: Callable,
point_sampler_args: dict,
candidate_point_sampler: Callable = None,
candidate_point_sampler_args: dict = None,
seed: int = None,
) -> np.ndarray:
"""
Expand All @@ -24,7 +28,8 @@ def euclidean(
Several Euclidean spaces can be considered. The possibilities are defined in the
:py:class:`~prefsampling.core.euclidean.EuclideanSpace` enumeration. You can also change the
dimension with the parameter :code:`dimension`.
dimension with the parameter :code:`dimension`. Note that you can specify different spaces for
the voters and for the candidates.
A collection of `num_voters` vote is generated independently and identically following the
process described above.
Expand All @@ -35,31 +40,45 @@ def euclidean(
Number of Voters.
num_candidates : int
Number of Candidates.
space : EuclideanSpace, default: :py:class:`~prefsampling.core.euclidean.EuclideanSpace.UNIFORM`
Type of space considered. Should be a constant defined in the
:py:class:`~prefsampling.core.euclidean.EuclideanSpace` enumeration.
dimension : int, default: `2`
Number of dimensions for the space considered
point_sampler : Callable
The sampler used to sample point in the space. Used for both voters and candidates
unless a `candidate_space` is provided.
point_sampler_args : dict
The arguments passed to the `point_sampler`. The argument `num_points` is ignored
and replaced by the number of voters or candidates.
candidate_point_sampler : Callable, default: :code:`None`
The sampler used to sample the points of the candidates. If a value is provided,
then the `space` argument is only used for voters.
candidate_point_sampler_args : dict
The arguments passed to the `candidate_point_sampler`. The argument `num_points`
is ignored and replaced by the number of candidates.
seed : int, default: :code:`None`
Seed for numpy random number generator.
Seed for numpy random number generator. Also passed to the point samplers if
a value is provided.
Returns
-------
np.ndarray
Ordinal votes.
"""
rng = np.random.default_rng(seed)
votes = np.zeros([num_voters, num_candidates], dtype=int)

voters, candidates = election_positions(
num_voters, num_candidates, space, dimension, rng
voters_pos, candidates_pos = sample_election_positions(
num_voters,
num_candidates,
point_sampler,
point_sampler_args,
candidate_point_sampler,
candidate_point_sampler_args,
seed,
)

dimension = len(voters_pos[0])
votes = np.zeros([num_voters, num_candidates], dtype=int)
distances = np.zeros([num_voters, num_candidates], dtype=float)
for i in range(num_voters):
for j in range(num_candidates):
distances[i][j] = np.linalg.norm(voters[i] - candidates[j], ord=dimension)
distances[i][j] = np.linalg.norm(voters_pos[i] - candidates_pos[j], ord=dimension)
votes[i] = np.argsort(distances[i])

return votes
11 changes: 11 additions & 0 deletions prefsampling/point/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from prefsampling.point.ball import ball
from prefsampling.point.sphere import sphere
from prefsampling.point.uniform import uniform
from prefsampling.point.gaussian import gaussian

__all__ = [
"uniform",
"gaussian",
"ball",
"sphere"
]
18 changes: 18 additions & 0 deletions prefsampling/point/ball.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import numpy as np

from prefsampling.inputvalidators import validate_int


def ball(num_points: int, dimension: int, center_point: float = 0.5, width: float = 1, seed: int = None):
validate_int(num_points, "num_points", 0)
validate_int(dimension, "dimension", 1)
rng = np.random.default_rng(seed)

points = []
for _ in range(num_points):
random_directions = rng.normal(size=(dimension, num_points))
random_directions /= np.linalg.norm(random_directions, axis=0)
random_radii = rng.random(num_points) ** (1 / dimension)
point = width * (random_directions * random_radii).T
points.append(point[0])
return np.array(points)
10 changes: 10 additions & 0 deletions prefsampling/point/gaussian.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import numpy as np

from prefsampling.inputvalidators import validate_int


def gaussian(num_points: int, dimension: int, center_point: float = 0.5, width: float = 0.15, seed: int = None):
validate_int(num_points, "num_points", 0)
validate_int(dimension, "dimension", 1)
rng = np.random.default_rng(seed)
return rng.normal(loc=center_point, scale=width, size=(num_points, dimension))
Loading

0 comments on commit dd8c858

Please sign in to comment.