Source code for mapof.elections.distances

import copy
import logging
from time import time

import mapof.core.persistence.experiment_exports as exports
import numpy as np
from mapof.core.inner_distances import map_str_to_func
from mapof.core.objects.Experiment import Experiment
from tqdm import tqdm

from mapof.elections.objects.ApprovalElection import ApprovalElection
from mapof.elections.objects.OrdinalElection import OrdinalElection

from mapof.elections.distances import feature_distance
from mapof.elections.distances import main_approval_distances as mad
from mapof.elections.distances import main_ordinal_distances as mod
from mapof.elections.distances import positionwise_infty

registered_approval_distances = {
    'approvalwise': mad.compute_approvalwise_distance,

    'hamming': mad.compute_approvalwise_distance,  # unsupported distance
}

registered_ordinal_distances = {
    'positionwise_infty': positionwise_infty.positionwise_size_independent,
    'feature_l1': feature_distance.features_vector_l1,
    'feature_l2': feature_distance.features_vector_l2,
    'positionwise': mod.compute_positionwise_distance,
    'bordawise': mod.compute_bordawise_distance,
    'pairwise': mod.compute_pairwise_distance,
    'discrete': mod.compute_discrete_distance,

    'swap': mod.compute_swap_distance,
    'spearman': mod.compute_spearman_distance,
    'spearman_aa': mod.compute_spearman_distance_fastmap,

    'blank': mod.compute_blank_distance,

    'ilp_spearman': mod.compute_spearman_distance_ilp_py,  # unsupported distance
    'voterlikeness': mod.compute_voterlikeness_distance,  # unsupported distance
    'agg_voterlikeness': mod.compute_agg_voterlikeness_distance,  # unsupported distance
    'pos_swap': mod.compute_pos_swap_distance,  # unsupported distance
    'voter_subelection': mod.compute_voter_subelection,  # unsupported distance
    'candidate_subelection': mod.compute_candidate_subelection,  # unsupported distance
}


def add_approval_distance(name: str, function: callable) -> None:
    """
    Adds a new approval distance to the list of approval distances.

    Parameters
    ----------
        name
            Name of the distance.
        function
            function that computes the distance.

    Returns
    -------
        None.
    """
    registered_approval_distances[name] = function


def add_ordinal_distance(name: str, function: callable) -> None:
    """
    Adds a new ordinal distance to the list of ordinal distances.

    Parameters
    ----------
        name
            Name of the distance.
        function
            function that computes the distance.

    Returns
    -------
        None.
    """
    registered_ordinal_distances[name] = function


[docs] def get_distance( election_1, election_2, distance_id: str = None ) -> float or (float, list): """ Computes distance between elections, (if applicable) optimal matching. Parameters ---------- election_1 First election. election_2 Second election. distance_id Name of the distance. """ if type(election_1) is ApprovalElection and type(election_2) is ApprovalElection: return get_approval_distance(election_1, election_2, distance_id=distance_id) elif type(election_1) is OrdinalElection and type(election_2) is OrdinalElection: return get_ordinal_distance(election_1, election_2, distance_id=distance_id) else: logging.warning('No such instance!')
[docs] def get_approval_distance( election_1: ApprovalElection, election_2: ApprovalElection, distance_id: str = None, **kwargs ) -> (float, list): """ Computes distance between approval elections, (if applicable) optimal matching. Parameters ---------- election_1 First election. election_2 Second election. distance_id Name of the distance. """ inner_distance, main_distance = _extract_distance_id(distance_id) if main_distance in registered_approval_distances: if inner_distance is not None: return registered_approval_distances.get(main_distance)(election_1, election_2, inner_distance) else: return registered_approval_distances.get(main_distance)(election_1, election_2, **kwargs) else: logging.warning(f'No such distance as: {main_distance}!')
[docs] def get_ordinal_distance( election_1: OrdinalElection, election_2: OrdinalElection, distance_id: str = None, **kwargs ) -> float or (float, list): """ Computes distance between ordinal elections, (if applicable) optimal matching. Parameters ---------- election_1 First election. election_2 Second election. distance_id Name of the distance. """ inner_distance, main_distance = _extract_distance_id(distance_id) if main_distance in registered_ordinal_distances: if inner_distance is not None: return registered_ordinal_distances.get(main_distance)(election_1, election_2, inner_distance) else: return registered_ordinal_distances.get(main_distance)(election_1, election_2, **kwargs) else: logging.warning(f'No such distance as: {main_distance}!')
def _extract_distance_id(distance_id: str) -> (callable, str): """ Return: inner distance (distance between votes) name and main distance name """ if '-' in distance_id: inner_distance, main_distance = distance_id.split('-') inner_distance = map_str_to_func(inner_distance) else: main_distance = distance_id inner_distance = None return inner_distance, main_distance
[docs] def run_single_process( exp: Experiment, instances_ids: list, distances: dict, times: dict, matchings: dict, safe_mode: bool = False ) -> None: """ Single process for computing distances """ for instance_id_1, instance_id_2 in tqdm(instances_ids, desc='Computing distances'): start_time = time() if safe_mode: distance = get_distance(copy.deepcopy(exp.instances[instance_id_1]), copy.deepcopy(exp.instances[instance_id_2]), distance_id=copy.deepcopy(exp.distance_id)) else: distance = get_distance(exp.instances[instance_id_1], exp.instances[instance_id_2], distance_id=exp.distance_id) if type(distance) is tuple: distance, matching = distance matching = np.array(matching) matchings[instance_id_1][instance_id_2] = matching matchings[instance_id_2][instance_id_1] = np.argsort(matching) distances[instance_id_1][instance_id_2] = distance distances[instance_id_2][instance_id_1] = distances[instance_id_1][instance_id_2] times[instance_id_1][instance_id_2] = time() - start_time times[instance_id_2][instance_id_1] = times[instance_id_1][instance_id_2]
[docs] def run_multiple_processes( experiment: Experiment, instances_ids: list, distances: dict, times: dict, matchings: dict, process_id: int ) -> None: """ Single process for computing distances """ for instance_id_1, instance_id_2 in tqdm(instances_ids, desc=f'Computing distances of thread {process_id}', position=process_id, leave=True): start_time = time() distance = get_distance(copy.deepcopy(experiment.instances[instance_id_1]), copy.deepcopy(experiment.instances[instance_id_2]), distance_id=copy.deepcopy(experiment.distance_id)) if type(distance) is tuple: distance, matching = distance matching = np.array(matching) matchings[instance_id_1][instance_id_2] = matching matchings[instance_id_2][instance_id_1] = np.argsort(matching) distances[instance_id_1][instance_id_2] = distance distances[instance_id_2][instance_id_1] = distances[instance_id_1][instance_id_2] times[instance_id_1][instance_id_2] = time() - start_time times[instance_id_2][instance_id_1] = times[instance_id_1][instance_id_2] if experiment.is_exported: exports.export_distances_multiple_processes(experiment, instances_ids, distances, times, process_id)
__all__ = [ 'get_distance', 'get_approval_distance', 'get_ordinal_distance', 'run_single_process', 'run_multiple_processes' ]