@@ -22,7 +22,11 @@ class EuclideanSpace(Enum):
22
22
UNBOUNDED_GAUSSIAN = "unbounded_gaussian"
23
23
24
24
25
- def euclidean_space_to_sampler (space : EuclideanSpace , num_dimensions : int ):
25
+ def euclidean_space_to_sampler (space : EuclideanSpace , num_dimensions : int ) -> (Callable , dict ):
26
+ """
27
+ Returns the point sampler together with its arguments corresponding to the EuclideanSpace
28
+ passed as argument.
29
+ """
26
30
if space == EuclideanSpace .UNIFORM_BALL :
27
31
return ball_uniform , {"num_dimensions" : num_dimensions }
28
32
if space == EuclideanSpace .UNIFORM_SPHERE :
@@ -52,45 +56,76 @@ def euclidean_space_to_sampler(space: EuclideanSpace, num_dimensions: int):
52
56
53
57
def _sample_points (
54
58
num_points : int ,
55
- sampler : Callable ,
56
- sampler_args : dict ,
57
- positions : Iterable [ float ] ,
59
+ num_dimensions : int ,
60
+ positions : EuclideanSpace | Callable | Iterable [ Iterable [ float ]] ,
61
+ positions_args : dict ,
58
62
sampled_object_name : str ,
59
63
) -> np .ndarray :
60
- if positions is None :
61
- if sampler is None :
62
- raise ValueError (
63
- f"You need to either provide a sampler for the { sampled_object_name } "
64
- f"or their positions."
64
+ """
65
+ Samples the points (if necessary) based on the input of the Euclidean function.
66
+ """
67
+ if isinstance (positions , Iterable ):
68
+ try :
69
+ positions = np .array (positions , dtype = float )
70
+ except Exception as e :
71
+ msg = (
72
+ "When trying to cast the provided positions to a numpy array, the above "
73
+ "exception occurred..."
65
74
)
66
- if sampler_args is None :
67
- sampler_args = dict ()
68
- sampler_args [ "num_points" ] = num_points
69
- positions = sampler ( ** sampler_args )
70
- else :
71
- positions = np . array ( positions , dtype = float )
72
- if len ( positions ) != num_points :
75
+ raise Exception ( msg ) from e
76
+
77
+ if num_dimensions == 1 :
78
+ expected_shape = ( num_points , )
79
+ else :
80
+ expected_shape = ( num_points , num_dimensions )
81
+ if positions . shape != expected_shape :
73
82
raise ValueError (
74
- f"The provided number of points does not match the number of "
75
- f"{ sampled_object_name } required ({ len (positions )} points provided for"
76
- f"{ num_points } { sampled_object_name } ."
83
+ f"The provided positions do not match the expected shape. Shape is "
84
+ f"{ positions .shape } while { expected_shape } was expected "
85
+ f"(num_{ sampled_object_name } , num_dimensions)."
86
+ )
87
+ return positions
88
+
89
+ if not isinstance (positions , Callable ):
90
+ try :
91
+ if isinstance (positions , Enum ):
92
+ space = EuclideanSpace (positions .value )
93
+ else :
94
+ space = EuclideanSpace (positions )
95
+ except Exception as e :
96
+ msg = (
97
+ f"If the positions for the { sampled_object_name } is not an Iterable (already, "
98
+ f"given positions) or a Callable (a sampler), then it should be a "
99
+ f"EuclideanSpace element. Casting the input to EuclideanSpace failed with the "
100
+ f"above exception."
77
101
)
102
+ raise Exception (msg ) from e
103
+
104
+ positions , new_positions_args = euclidean_space_to_sampler (space , num_dimensions )
105
+ new_positions_args .update (positions_args )
106
+ positions_args = new_positions_args
107
+ positions_args ["num_points" ] = num_points
108
+ positions = np .array (positions (** positions_args ))
109
+
110
+ if positions .shape != (num_points , num_dimensions ):
111
+ raise ValueError (
112
+ "After sampling the position, the obtained shape is not as expected. "
113
+ f"Shape is { positions .shape } while { (num_points , num_dimensions )} was "
114
+ f"expected (num_{ sampled_object_name } , num_dimensions)."
115
+ )
116
+
78
117
return positions
79
118
80
119
81
120
@validate_num_voters_candidates
82
121
def sample_election_positions (
83
122
num_voters : int ,
84
123
num_candidates : int ,
85
- euclidean_space : EuclideanSpace = None ,
86
- candidate_euclidean_space : EuclideanSpace = None ,
87
- num_dimensions : int = None ,
88
- point_sampler : Callable = None ,
89
- point_sampler_args : dict = None ,
90
- candidate_point_sampler : Callable = None ,
91
- candidate_point_sampler_args : dict = None ,
92
- voters_positions : Iterable [Iterable [float ]] = None ,
93
- candidates_positions : Iterable [Iterable [float ]] = None ,
124
+ num_dimensions : int ,
125
+ voters_positions : EuclideanSpace | Callable | Iterable [Iterable [float ]],
126
+ candidates_positions : EuclideanSpace | Callable | Iterable [Iterable [float ]],
127
+ voters_positions_args : dict = None ,
128
+ candidates_positions_args : dict = None ,
94
129
seed : int = None ,
95
130
) -> tuple [np .ndarray , np .ndarray ]:
96
131
"""
@@ -101,36 +136,32 @@ def sample_election_positions(
101
136
Number of Voters.
102
137
num_candidates : int
103
138
Number of Candidates.
104
- euclidean_space: EuclideanSpace, default: :code:`None`
105
- Use a pre-defined Euclidean space for sampling the position of the voters. If no
106
- `candidate_euclidean_space` is provided, the value of 'euclidean_space' is used for the
107
- candidates as well. A number of dimension needs to be provided.
108
- candidate_euclidean_space: EuclideanSpace, default: :code:`None`
109
- Use a pre-defined Euclidean space for sampling the position of the candidates. If no
110
- value is provided, the value of 'euclidean_space' is used. A number of dimension needs
111
- to be provided.
112
- num_dimensions: int, default: :code:`None`
139
+ num_dimensions: int
113
140
The number of dimensions to use. Using this argument is mandatory when passing a space
114
141
as argument. If you pass samplers as arguments and use the num_dimensions, then, the
115
142
value of num_dimensions is passed as a kwarg to the samplers.
116
- point_sampler : Callable, default: :code:`None`
117
- The sampler used to sample point in the space. It should be a function accepting
118
- arguments 'num_points' and 'seed'. Used for both voters and candidates unless a
119
- `candidate_space` is provided.
120
- point_sampler_args : dict, default: :code:`None`
121
- The arguments passed to the `point_sampler`. The argument `num_points` is ignored
122
- and replaced by the number of voters or candidates.
123
- candidate_point_sampler : Callable, default: :code:`None`
124
- The sampler used to sample the points of the candidates. It should be a function
125
- accepting arguments 'num_points' and 'seed'. If a value is provided, then the
126
- `point_sampler_args` argument is only used for voters.
127
- candidate_point_sampler_args : dict
128
- The arguments passed to the `candidate_point_sampler`. The argument `num_points`
129
- is ignored and replaced by the number of candidates.
130
- voters_positions : Iterable[Iterable[float]]
131
- Position of the voters.
132
- candidates_positions : Iterable[Iterable[float]]
133
- Position of the candidates.
143
+ voters_positions: py:class:`~prefsampling.core.euclidean.EuclideanSpace` | Callable | Iterable[Iterable[float]]
144
+ The positions of the voters, or a way to determine them. If an Iterable is passed,
145
+ then it is assumed to be the positions themselves. Otherwise, it is assumed that a
146
+ sampler for the positions is passed. It can be either the nickname of a sampler---when
147
+ passing a py:class:`~prefsampling.core.euclidean.EuclideanSpace`; or a sampler.
148
+ A sampler is a function that takes as keywords arguments: 'num_points',
149
+ 'num_dimensions', and 'seed'. Additional arguments can be provided with by using the
150
+ :code:`voters_positions_args` argument.
151
+ candidates_positions: py:class:`~prefsampling.core.euclidean.EuclideanSpace` | Callable | Iterable[Iterable[float]]
152
+ The positions of the candidates, or a way to determine them. If an Iterable is passed,
153
+ then it is assumed to be the positions themselves. Otherwise, it is assumed that a
154
+ sampler for the positions is passed. It can be either the nickname of a sampler---when
155
+ passing a py:class:`~prefsampling.core.euclidean.EuclideanSpace`; or a sampler.
156
+ A sampler is a function that takes as keywords arguments: 'num_points',
157
+ 'num_dimensions', and 'seed'. Additional arguments can be provided with by using the
158
+ :code:`candidates_positions_args` argument.
159
+ voters_positions_args: dict, default: :code:`dict()`
160
+ Additional keyword arguments passed to the :code:`voters_positions` sampler when the
161
+ latter is a Callable.
162
+ candidates_positions_args: dict, default: :code:`dict()`
163
+ Additional keyword arguments passed to the :code:`candidates_positions` sampler when the
164
+ latter is a Callable.
134
165
seed : int, default: :code:`None`
135
166
Seed for numpy random number generator. Also passed to the point samplers if
136
167
a value is provided.
@@ -141,66 +172,26 @@ def sample_election_positions(
141
172
The positions of the voters and of the candidates.
142
173
143
174
"""
144
- if euclidean_space :
145
- if num_dimensions is None :
146
- raise ValueError (
147
- "If you are using the 'euclidean_space' argument, you need to also "
148
- "provide a number of dimensions."
149
- )
150
- validate_int (num_dimensions , "number of dimensions" , 1 )
151
- if isinstance (euclidean_space , Enum ):
152
- euclidean_space = EuclideanSpace (euclidean_space .value )
153
- else :
154
- euclidean_space = EuclideanSpace (euclidean_space )
155
-
156
- point_sampler , point_sampler_args = euclidean_space_to_sampler (
157
- euclidean_space , num_dimensions
158
- )
159
- if candidate_euclidean_space :
160
- if num_dimensions is None :
161
- raise ValueError (
162
- "If you are using the 'candidate_euclidean_space' argument, you need "
163
- "to also provide a number of dimensions."
164
- )
165
- if isinstance (candidate_euclidean_space , Enum ):
166
- candidate_euclidean_space = EuclideanSpace (candidate_euclidean_space .value )
167
- else :
168
- candidate_euclidean_space = EuclideanSpace (candidate_euclidean_space )
169
-
170
- candidate_point_sampler , candidate_point_sampler_args = (
171
- euclidean_space_to_sampler (candidate_euclidean_space , num_dimensions )
172
- )
173
-
174
- if point_sampler_args is None :
175
- point_sampler_args = dict ()
175
+ validate_int (num_dimensions , lower_bound = 0 , value_descr = "number of dimensions" )
176
+ if voters_positions_args is None :
177
+ voters_positions_args = dict ()
178
+ if candidates_positions_args is None :
179
+ candidates_positions_args = dict ()
176
180
if seed is not None :
177
- point_sampler_args ["seed" ] = seed
178
- if candidate_point_sampler is not None :
179
- candidate_point_sampler_args ["seed" ] = seed
180
- if num_dimensions is not None :
181
- point_sampler_args ["num_dimensions" ] = num_dimensions
182
- if candidate_point_sampler is not None :
183
- candidate_point_sampler_args ["num_dimensions" ] = num_dimensions
181
+ voters_positions_args ["seed" ] = seed
182
+ candidates_positions_args ["seed" ] = seed
183
+
184
+ voters_positions_args ["num_dimensions" ] = num_dimensions
185
+ candidates_positions_args ["num_dimensions" ] = num_dimensions
184
186
185
187
voters_pos = _sample_points (
186
- num_voters , point_sampler , point_sampler_args , voters_positions , "voters"
188
+ num_voters , num_dimensions , voters_positions , voters_positions_args , "voters"
187
189
)
188
- dimension = len (voters_pos [0 ])
189
- if candidate_point_sampler :
190
- point_sampler = candidate_point_sampler
191
- point_sampler_args = candidate_point_sampler_args
192
190
cand_pos = _sample_points (
193
191
num_candidates ,
194
- point_sampler ,
195
- point_sampler_args ,
192
+ num_dimensions ,
196
193
candidates_positions ,
194
+ candidates_positions_args ,
197
195
"candidates" ,
198
196
)
199
-
200
- if len (cand_pos [0 ]) != dimension :
201
- raise ValueError (
202
- "The position of the voters and of the candidates do not have the same dimension ("
203
- f"{ dimension } for the voters and { len (cand_pos [0 ])} for the candidates)."
204
- )
205
-
206
197
return voters_pos , cand_pos
0 commit comments