Skip to content

Commit dd8c858

Browse files
committed
Euclidean updated
1 parent 29261d7 commit dd8c858

File tree

13 files changed

+255
-160
lines changed

13 files changed

+255
-160
lines changed

prefsampling/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
from itertools import chain
77

88
from prefsampling.approval import NoiseType
9-
from prefsampling.core.euclidean import EuclideanSpace
109
from prefsampling.ordinal import TreeSampler
1110

1211

1312
class CONSTANTS(Enum):
1413
""" All constants of the package """
1514
_ignore_ = 'member cls'
1615
cls = vars()
17-
for member in chain(list(EuclideanSpace), list(TreeSampler), list(NoiseType)):
16+
for member in chain(list(TreeSampler), list(NoiseType)):
1817
if member.name in cls:
1918
raise ValueError(f"The name {member.name} is used in more than one enumeration. The"
2019
f"CONSTANTS class needs unique names to be well-defined.")

prefsampling/approval/euclidean.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
from __future__ import annotations
22

3+
from collections.abc import Callable
4+
35
import numpy as np
46

5-
from prefsampling.core.euclidean import election_positions, EuclideanSpace
7+
from prefsampling.core.euclidean import sample_election_positions
68
from prefsampling.inputvalidators import validate_num_voters_candidates
79

810

911
@validate_num_voters_candidates
1012
def euclidean(
1113
num_voters: int,
1214
num_candidates: int,
15+
point_sampler: Callable,
16+
point_sampler_args: dict,
17+
candidate_point_sampler: Callable = None,
18+
candidate_point_sampler_args: dict = None,
1319
radius: float = 0.5,
14-
space: EuclideanSpace = EuclideanSpace.UNIFORM,
15-
dimension: int = 2,
1620
seed: int = None,
1721
) -> list[set[int]]:
1822
"""
@@ -35,29 +39,44 @@ def euclidean(
3539
Number of Voters.
3640
num_candidates : int
3741
Number of Candidates.
42+
point_sampler : Callable
43+
The sampler used to sample point in the space. Used for both voters and candidates
44+
unless a `candidate_space` is provided.
45+
point_sampler_args : dict
46+
The arguments passed to the `point_sampler`. The argument `num_points` is ignored
47+
and replaced by the number of voters or candidates.
48+
candidate_point_sampler : Callable, default: :code:`None`
49+
The sampler used to sample the points of the candidates. If a value is provided,
50+
then the `space` argument is only used for voters.
51+
candidate_point_sampler_args : dict
52+
The arguments passed to the `candidate_point_sampler`. The argument `num_points`
53+
is ignored and replaced by the number of candidates.
3854
radius : float, default: 0.5
3955
Radius of approval.
40-
space : EuclideanSpace, default: :py:const:`~prefsampling.core.euclidean.EuclideanSpace.UNIFORM`
41-
Type of space considered. Should be a constant defined in the
42-
:py:class:`~prefsampling.core.euclidean.EuclideanSpace`.
43-
dimension : int, default: 2
44-
Number of Dimensions.
45-
seed : int
46-
Seed for numpy random number generator.
56+
seed : int, default: :code:`None`
57+
Seed for numpy random number generator. Also passed to the point samplers if
58+
a value is provided.
4759
4860
Returns
4961
-------
5062
list[set[int]]
5163
Approval votes.
5264
"""
53-
rng = np.random.default_rng(seed)
54-
votes = [set() for _ in range(num_voters)]
55-
voters, candidates = election_positions(
56-
num_voters, num_candidates, space, dimension, rng
65+
66+
voters_pos, candidates_pos = sample_election_positions(
67+
num_voters,
68+
num_candidates,
69+
point_sampler,
70+
point_sampler_args,
71+
candidate_point_sampler,
72+
candidate_point_sampler_args,
73+
seed,
5774
)
75+
76+
votes = [set() for _ in range(num_voters)]
5877
for v in range(num_voters):
5978
for c in range(num_candidates):
60-
if radius >= np.linalg.norm(voters[v] - candidates[c]):
79+
if radius >= np.linalg.norm(voters_pos[v] - candidates_pos[c]):
6180
votes[v].add(c)
6281

6382
return votes

prefsampling/core/euclidean.py

Lines changed: 52 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,73 @@
11
from __future__ import annotations
22

3-
from enum import Enum
3+
from collections.abc import Callable
44

55
import numpy as np
66

77
from prefsampling.inputvalidators import validate_num_voters_candidates
88

99

10-
class EuclideanSpace(Enum):
11-
"""
12-
Constants used to represent Euclidean spaces
13-
"""
14-
15-
UNIFORM = "Uniform Space"
16-
"""
17-
Uniform space
18-
"""
19-
GAUSSIAN = "Gaussian Space"
20-
"""
21-
Gaussian space
22-
"""
23-
SPHERE = "Spherical Space"
24-
"""
25-
Spherical space
26-
"""
27-
BALL = "Ball Space"
28-
"""
29-
Ball-shaped space
30-
"""
31-
32-
3310
@validate_num_voters_candidates
34-
def election_positions(
11+
def sample_election_positions(
3512
num_voters: int,
3613
num_candidates: int,
37-
space: EuclideanSpace,
38-
dimension: int,
39-
rng: np.random.Generator,
40-
) -> (np.ndarray, np.ndarray):
14+
point_sampler: Callable,
15+
point_sampler_args: dict,
16+
candidate_point_sampler: Callable = None,
17+
candidate_point_sampler_args: dict = None,
18+
seed: int = None,
19+
) -> tuple[np.ndarray, np.ndarray]:
4120
"""
42-
Returns the position of the voters and the candidates in a Euclidean space.
4321
4422
Parameters
4523
----------
46-
num_voters: int
47-
The number of voters.
48-
num_candidates: int
49-
The number of candidates.
50-
space : :py:class:`~prefsampling.core.euclidean.EuclideanSpace`
51-
Type of space considered. Should be a constant defined in the
52-
:py:class:`~prefsampling.core.euclidean.EuclideanSpace`.
53-
dimension : int
54-
Number of dimensions for the space considered.
55-
rng : np.random.Generator
56-
The numpy generator to use for randomness.
24+
num_voters : int
25+
Number of Voters.
26+
num_candidates : int
27+
Number of Candidates.
28+
point_sampler : Callable
29+
The sampler used to sample point in the space. Used for both voters and candidates
30+
unless a `candidate_space` is provided.
31+
point_sampler_args : dict
32+
The arguments passed to the `point_sampler`. The argument `num_points` is ignored
33+
and replaced by the number of voters or candidates.
34+
candidate_point_sampler : Callable, default: :code:`None`
35+
The sampler used to sample the points of the candidates. If a value is provided,
36+
then the `space` argument is only used for voters.
37+
candidate_point_sampler_args : dict
38+
The arguments passed to the `candidate_point_sampler`. The argument `num_points`
39+
is ignored and replaced by the number of candidates.
40+
seed : int, default: :code:`None`
41+
Seed for numpy random number generator. Also passed to the point samplers if
42+
a value is provided.
5743
5844
Returns
5945
-------
60-
(np.ndarray, np.ndarray)
61-
The position of the voters and of the candidates respectively.
62-
"""
63-
if isinstance(space, Enum):
64-
space = EuclideanSpace(space.value)
65-
else:
66-
space = EuclideanSpace(space)
67-
if space == EuclideanSpace.UNIFORM:
68-
voters = rng.random((num_voters, dimension))
69-
candidates = rng.random((num_candidates, dimension))
70-
elif space == EuclideanSpace.GAUSSIAN:
71-
voters = rng.normal(loc=0.5, scale=0.15, size=(num_voters, dimension))
72-
candidates = rng.normal(loc=0.5, scale=0.15, size=(num_candidates, dimension))
73-
elif space == EuclideanSpace.SPHERE:
74-
voters = np.array(
75-
[list(random_sphere(dimension, rng)[0]) for _ in range(num_voters)]
76-
)
77-
candidates = np.array(
78-
[list(random_sphere(dimension, rng)[0]) for _ in range(num_candidates)]
79-
)
80-
elif space == EuclideanSpace.BALL:
81-
voters = np.array(
82-
[list(random_ball(dimension, rng)[0]) for _ in range(num_voters)]
83-
)
84-
candidates = np.array(
85-
[list(random_ball(dimension, rng)[0]) for _ in range(num_candidates)]
86-
)
87-
else:
88-
raise ValueError(
89-
"The `space` argument needs to be one of the constant defined in the "
90-
"core.euclidean.EuclideanSpace enumeration. Choices are: "
91-
+ ", ".join(str(s) for s in EuclideanSpace)
92-
)
93-
return voters, candidates
94-
46+
tuple[np.ndarray, np.ndarray]
47+
The positions of the voters and of the candidates.
9548
96-
def random_ball(
97-
dimension: int, rng: np.random.Generator, num_points: int = 1, radius: float = 1
98-
) -> np.ndarray:
99-
random_directions = rng.normal(size=(dimension, num_points))
100-
random_directions /= np.linalg.norm(random_directions, axis=0)
101-
random_radii = rng.random(num_points) ** (1 / dimension)
102-
x = radius * (random_directions * random_radii).T
103-
return x
49+
"""
50+
if candidate_point_sampler is not None and candidate_point_sampler_args is None:
51+
raise ValueError("If candidate_point_sampler is not None, a value needs to be "
52+
"passed to candidate_point_sampler_args (even if it's just "
53+
"an empty dictionary).")
10454

55+
if seed is not None:
56+
point_sampler_args["seed"] = seed
57+
if candidate_point_sampler is not None:
58+
candidate_point_sampler_args["seed"] = seed
10559

106-
def random_sphere(
107-
dimension: int, rng: np.random.Generator, num_points: int = 1, radius: float = 1
108-
) -> np.ndarray:
109-
random_directions = rng.normal(size=(dimension, num_points))
110-
random_directions /= np.linalg.norm(random_directions, axis=0)
111-
random_radii = 1.0
112-
return radius * (random_directions * random_radii).T
60+
point_sampler_args['num_points'] = num_voters
61+
voters_pos = point_sampler(**point_sampler_args)
62+
dimension = len(voters_pos[0])
63+
if candidate_point_sampler is None:
64+
point_sampler_args['num_points'] = num_candidates
65+
candidates_pos = point_sampler(**point_sampler_args)
66+
else:
67+
candidate_point_sampler_args['num_points'] = num_candidates
68+
candidates_pos = candidate_point_sampler(**candidate_point_sampler_args)
69+
if len(candidates_pos[0]) != dimension:
70+
raise ValueError("The position of the voters and of the candidates do not have the "
71+
"same dimension. Use different point samplers to solve this "
72+
"problem.")
73+
return voters_pos, candidates_pos

prefsampling/ordinal/euclidean.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
from __future__ import annotations
22

3+
from collections.abc import Callable
4+
35
import numpy as np
46
from numpy import linalg
57

6-
from prefsampling.core.euclidean import election_positions, EuclideanSpace
8+
from prefsampling.core.euclidean import sample_election_positions
79
from prefsampling.inputvalidators import validate_num_voters_candidates
810

911

1012
@validate_num_voters_candidates
1113
def euclidean(
1214
num_voters: int,
1315
num_candidates: int,
14-
space: EuclideanSpace = EuclideanSpace.UNIFORM,
15-
dimension: int = 2,
16+
point_sampler: Callable,
17+
point_sampler_args: dict,
18+
candidate_point_sampler: Callable = None,
19+
candidate_point_sampler_args: dict = None,
1620
seed: int = None,
1721
) -> np.ndarray:
1822
"""
@@ -24,7 +28,8 @@ def euclidean(
2428
2529
Several Euclidean spaces can be considered. The possibilities are defined in the
2630
:py:class:`~prefsampling.core.euclidean.EuclideanSpace` enumeration. You can also change the
27-
dimension with the parameter :code:`dimension`.
31+
dimension with the parameter :code:`dimension`. Note that you can specify different spaces for
32+
the voters and for the candidates.
2833
2934
A collection of `num_voters` vote is generated independently and identically following the
3035
process described above.
@@ -35,31 +40,45 @@ def euclidean(
3540
Number of Voters.
3641
num_candidates : int
3742
Number of Candidates.
38-
space : EuclideanSpace, default: :py:class:`~prefsampling.core.euclidean.EuclideanSpace.UNIFORM`
39-
Type of space considered. Should be a constant defined in the
40-
:py:class:`~prefsampling.core.euclidean.EuclideanSpace` enumeration.
41-
dimension : int, default: `2`
42-
Number of dimensions for the space considered
43+
point_sampler : Callable
44+
The sampler used to sample point in the space. Used for both voters and candidates
45+
unless a `candidate_space` is provided.
46+
point_sampler_args : dict
47+
The arguments passed to the `point_sampler`. The argument `num_points` is ignored
48+
and replaced by the number of voters or candidates.
49+
candidate_point_sampler : Callable, default: :code:`None`
50+
The sampler used to sample the points of the candidates. If a value is provided,
51+
then the `space` argument is only used for voters.
52+
candidate_point_sampler_args : dict
53+
The arguments passed to the `candidate_point_sampler`. The argument `num_points`
54+
is ignored and replaced by the number of candidates.
4355
seed : int, default: :code:`None`
44-
Seed for numpy random number generator.
56+
Seed for numpy random number generator. Also passed to the point samplers if
57+
a value is provided.
4558
4659
Returns
4760
-------
4861
np.ndarray
4962
Ordinal votes.
5063
5164
"""
52-
rng = np.random.default_rng(seed)
53-
votes = np.zeros([num_voters, num_candidates], dtype=int)
5465

55-
voters, candidates = election_positions(
56-
num_voters, num_candidates, space, dimension, rng
66+
voters_pos, candidates_pos = sample_election_positions(
67+
num_voters,
68+
num_candidates,
69+
point_sampler,
70+
point_sampler_args,
71+
candidate_point_sampler,
72+
candidate_point_sampler_args,
73+
seed,
5774
)
5875

76+
dimension = len(voters_pos[0])
77+
votes = np.zeros([num_voters, num_candidates], dtype=int)
5978
distances = np.zeros([num_voters, num_candidates], dtype=float)
6079
for i in range(num_voters):
6180
for j in range(num_candidates):
62-
distances[i][j] = np.linalg.norm(voters[i] - candidates[j], ord=dimension)
81+
distances[i][j] = np.linalg.norm(voters_pos[i] - candidates_pos[j], ord=dimension)
6382
votes[i] = np.argsort(distances[i])
6483

6584
return votes

prefsampling/point/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from prefsampling.point.ball import ball
2+
from prefsampling.point.sphere import sphere
3+
from prefsampling.point.uniform import uniform
4+
from prefsampling.point.gaussian import gaussian
5+
6+
__all__ = [
7+
"uniform",
8+
"gaussian",
9+
"ball",
10+
"sphere"
11+
]

prefsampling/point/ball.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import numpy as np
2+
3+
from prefsampling.inputvalidators import validate_int
4+
5+
6+
def ball(num_points: int, dimension: int, center_point: float = 0.5, width: float = 1, seed: int = None):
7+
validate_int(num_points, "num_points", 0)
8+
validate_int(dimension, "dimension", 1)
9+
rng = np.random.default_rng(seed)
10+
11+
points = []
12+
for _ in range(num_points):
13+
random_directions = rng.normal(size=(dimension, num_points))
14+
random_directions /= np.linalg.norm(random_directions, axis=0)
15+
random_radii = rng.random(num_points) ** (1 / dimension)
16+
point = width * (random_directions * random_radii).T
17+
points.append(point[0])
18+
return np.array(points)

prefsampling/point/gaussian.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import numpy as np
2+
3+
from prefsampling.inputvalidators import validate_int
4+
5+
6+
def gaussian(num_points: int, dimension: int, center_point: float = 0.5, width: float = 0.15, seed: int = None):
7+
validate_int(num_points, "num_points", 0)
8+
validate_int(dimension, "dimension", 1)
9+
rng = np.random.default_rng(seed)
10+
return rng.normal(loc=center_point, scale=width, size=(num_points, dimension))

0 commit comments

Comments
 (0)