Source code for prefsampling.core.euclidean

from __future__ import annotations

from collections.abc import Callable, Iterable
from enum import Enum

import numpy as np

from prefsampling.inputvalidators import validate_num_voters_candidates, validate_int
from prefsampling.point import ball_uniform, cube, ball_resampling, gaussian


[docs] class EuclideanSpace(Enum): """ Constants for some pre-defined Euclidean distributions. """ UNIFORM_BALL = "uniform_ball" """ Constants representing a uniform ball with center point at the origin and width of 1 for all dimensions. """ UNIFORM_SPHERE = "uniform_sphere" """ Constants representing a uniform sphere with center point at the origin and width of 1 for all dimensions. This is the envelope of the uniform ball. """ UNIFORM_CUBE = "uniform_cube" """ Constants representing a uniform cube with center point at the origin and width of 1 for all dimensions. """ GAUSSIAN_BALL = "gaussian_ball" """ Constants representing a Gaussian ball with center point at the origin and width of 1 for all dimensions. The inner Gaussian sampler has mean 0 and standard deviation 0.33. """ GAUSSIAN_CUBE = "gaussian_cube" """ Constants representing a Gaussian ball with center point at the origin and width of 1 for all dimensions. """ UNBOUNDED_GAUSSIAN = "unbounded_gaussian" """ Constants representing an unbounded Gaussian space.The inner Gaussian sampler has mean 0 and standard deviation 1. """
[docs] def euclidean_space_to_sampler( space: EuclideanSpace, num_dimensions: int, seed: int = None ) -> (Callable, dict): """ Returns the point sampler together with its arguments corresponding to the EuclideanSpace passed as argument. """ if space == EuclideanSpace.UNIFORM_BALL: return ball_uniform, {"num_dimensions": num_dimensions, "seed": seed} if space == EuclideanSpace.UNIFORM_SPHERE: return ball_uniform, { "num_dimensions": num_dimensions, "only_envelope": True, "seed": seed, } if space == EuclideanSpace.UNIFORM_CUBE: return cube, {"num_dimensions": num_dimensions, "seed": seed} if space == EuclideanSpace.GAUSSIAN_BALL: return ball_resampling, { "num_dimensions": num_dimensions, "inner_sampler": lambda **kwargs: gaussian(**kwargs)[0], "inner_sampler_args": { "num_dimensions": num_dimensions, "num_points": 1, "sigmas": [0.33] * num_dimensions, "seed": seed, }, "seed": seed, } if space == EuclideanSpace.GAUSSIAN_CUBE: return gaussian, { "num_dimensions": num_dimensions, "widths": np.array([1 for _ in range(num_dimensions)]), "seed": seed, } if space == EuclideanSpace.UNBOUNDED_GAUSSIAN: return gaussian, {"num_dimensions": num_dimensions, "seed": seed} raise ValueError( "The 'euclidean_space' and/or the 'candidate_euclidean_space' arguments need to be one of " "the constant defined in the core.euclidean.EuclideanSpace enumeration. Choices are: " + ", ".join(str(s) for s in EuclideanSpace) + "." )
def _sample_points( num_points: int, num_dimensions: int, positions: EuclideanSpace | Callable | Iterable[Iterable[float]], positions_args: dict, sampled_object_name: str, seed=None, ) -> np.ndarray: """ Samples the points (if necessary) based on the input of the Euclidean function. """ if isinstance(positions, Iterable) and not isinstance(positions, str): try: positions = np.array(positions, dtype=float) except Exception as e: msg = ( "When trying to cast the provided positions to a numpy array, the above " "exception occurred..." ) raise ValueError(msg) from e if positions.shape != (num_points, num_dimensions): if num_dimensions > 1 or positions.shape != (num_points,): raise ValueError( f"The provided positions do not match the expected shape. Shape is " f"{positions.shape} while {(num_points, num_dimensions)} was expected " f"(num_{sampled_object_name}, num_dimensions)." ) return positions if not isinstance(positions, Callable): try: if isinstance(positions, Enum): space = EuclideanSpace(positions.value) else: space = EuclideanSpace(positions) except Exception as e: msg = ( f"If the positions for the {sampled_object_name} is not an Iterable (already, " f"given positions) or a Callable (a sampler), then it should be a " f"EuclideanSpace element. Casting the input to EuclideanSpace failed with the " f"above exception." ) raise ValueError(msg) from e positions, new_positions_args = euclidean_space_to_sampler( space, num_dimensions, seed=seed ) new_positions_args.update(positions_args) positions_args = new_positions_args positions_args["seed"] = seed positions_args["num_points"] = num_points positions = np.array(positions(**positions_args)) if positions.shape != (num_points, num_dimensions): raise ValueError( "After sampling the position, the obtained shape is not as expected. " f"Shape is {positions.shape} while {(num_points, num_dimensions)} was " f"expected (num_{sampled_object_name}, num_dimensions)." ) return positions
[docs] @validate_num_voters_candidates def sample_election_positions( num_voters: int, num_candidates: int, num_dimensions: int, voters_positions: EuclideanSpace | Callable | Iterable[Iterable[float]], candidates_positions: EuclideanSpace | Callable | Iterable[Iterable[float]], voters_positions_args: dict = None, candidates_positions_args: dict = None, seed: int = None, ) -> tuple[np.ndarray, np.ndarray]: """ Parameters ---------- num_voters : int Number of Voters. num_candidates : int Number of Candidates. num_dimensions: int The number of dimensions to use. Using this argument is mandatory when passing a space as argument. If you pass samplers as arguments and use the num_dimensions, then, the value of num_dimensions is passed as a kwarg to the samplers. voters_positions: :py:class:`~prefsampling.core.euclidean.EuclideanSpace` | Callable | Iterable[Iterable[float]] The positions of the voters, or a way to determine them. If an Iterable is passed, then it is assumed to be the positions themselves. Otherwise, it is assumed that a sampler for the positions is passed. It can be either the nickname of a sampler---when passing a :py:class:`~prefsampling.core.euclidean.EuclideanSpace`; or a sampler. A sampler is a function that takes as keywords arguments: 'num_points', 'num_dimensions', and 'seed'. Additional arguments can be provided with by using the :code:`voters_positions_args` argument. candidates_positions: :py:class:`~prefsampling.core.euclidean.EuclideanSpace` | Callable | Iterable[Iterable[float]] The positions of the candidates, or a way to determine them. If an Iterable is passed, then it is assumed to be the positions themselves. Otherwise, it is assumed that a sampler for the positions is passed. It can be either the nickname of a sampler---when passing a :py:class:`~prefsampling.core.euclidean.EuclideanSpace`; or a sampler. A sampler is a function that takes as keywords arguments: 'num_points', 'num_dimensions', and 'seed'. Additional arguments can be provided with by using the :code:`candidates_positions_args` argument. voters_positions_args: dict, default: :code:`dict()` Additional keyword arguments passed to the :code:`voters_positions` sampler when the latter is a Callable. candidates_positions_args: dict, default: :code:`dict()` Additional keyword arguments passed to the :code:`candidates_positions` sampler when the latter is a Callable. seed : int, default: :code:`None` Seed for numpy random number generator. Also passed to the point samplers if a value is provided. Returns ------- tuple[np.ndarray, np.ndarray] The positions of the voters and of the candidates. """ validate_int(num_dimensions, lower_bound=0, value_descr="number of dimensions") if voters_positions_args is None: voters_positions_args = dict() if candidates_positions_args is None: candidates_positions_args = dict() voters_positions_args["num_dimensions"] = num_dimensions candidates_positions_args["num_dimensions"] = num_dimensions voters_pos = _sample_points( num_voters, num_dimensions, voters_positions, voters_positions_args, "voters", seed=seed, ) cand_pos = _sample_points( num_candidates, num_dimensions, candidates_positions, candidates_positions_args, "candidates", seed=seed, ) return voters_pos, cand_pos