Exemple #1
0
def test_connected_component():
    """Test that connected component works"""
    simset = collect.mcces(1, [([0], 0)])
    assert simset.add([1.5], 1)
    assert not simset.add([0.75], .5)
    assert len(simset) == 1
    assert [((0,), 0)] == list(simset)
    assert (repr(simset) ==
            'MinimumConnectedComponentElementSet(1, [((0,), 0)])')

    simset.clear()
    assert simset.add([0.75], 1)
    assert not simset.add([1.5], 0.5)
    assert not simset.add([0], 0)
    assert [((0,), 0)] == list(simset)

    simset.clear()
    assert simset.add([0], 0)
    assert not simset.add([0.75], 1)
    assert not simset.add([1.5], 0.5)
    assert simset.add([3], 2)
    assert [((0,), 0), ((3,), 2)] == list(simset)

    assert [1.5] in simset
    assert [-0.5] in simset
    assert [5] not in simset
Exemple #2
0
def process_game(args):  # pylint: disable=too-many-locals
    """Compute information about a game"""
    i, (name, game) = args
    np.random.seed(i)  # Reproducible randomness
    profiles = gen_profiles(game)

    reg_thresh = 1e-2  # FIXME
    conv_thresh = 1e-2 * np.sqrt(2 * game.num_roles)  # FIXME

    all_eqa = collect.mcces(conv_thresh)
    meth_times = {}
    meth_eqa = {}
    for method, single, func in gen_methods():
        logging.warning('Starting {} for {} {:d}'.format(method, name, i))
        prof_times = {}
        prof_eqa = {}
        for prof, mix_gen in profiles.items():
            times = []
            eqa = []
            if prof != 'uniform' and single:
                continue
            for mix in mix_gen():
                start = time.time()
                eqm = func(game, mix)
                speed = time.time() - start
                reg = regret.mixture_regret(game, eqm)
                times.append(speed)
                if reg < reg_thresh:
                    all_eqa.add(eqm, reg)
                    eqa.append(eqm)
            prof_times[prof] = times
            prof_eqa[prof] = eqa
        meth_times[method] = prof_times
        meth_eqa[method] = prof_eqa
        logging.warning('Finished {} for {} {:d} - took {:f} seconds'.format(
            method, name, i, speed))

    inds = {}
    for norm, _ in all_eqa:
        inds[norm] = len(inds)

    for prof_eqa in meth_eqa.values():
        for prof, eqa in prof_eqa.items():
            prof_eqa[prof] = list({inds[all_eqa.get(e)] for e in eqa})

    return name, meth_times, meth_eqa
Exemple #3
0
def mixed_nash(  # pylint: disable=too-many-locals
        game,
        *,
        regret_thresh=1e-3,
        dist_thresh=0.1,
        grid_points=2,
        random_restarts=0,
        processes=0,
        min_reg=False,
        at_least_one=False,
        **methods):
    """Finds role-symmetric mixed Nash equilibria

    This is the intended front end for nash equilibria finding, wrapping the
    individual methods in a convenient front end that also support parallel
    execution. Scipy optimize, and hence nash finding with the optimize method
    is NOT thread safe. This can be mitigated by running nash finding in a
    separate process (by setting processes > 0) if the game is pickleable.

    This is the old style nash finding and provides more options. For new
    methods, mixture_equilibria is the preferred interface.

    Arguments
    ---------
    regret_thresh : float, optional
        The threshold to consider an equilibrium found.
    dist_thresh : float, optional
        The threshold for considering equilibria distinct.
    grid_points : int > 1, optional
        The number of grid points to use for mixture seeds. two implies just
        pure mixtures, more will be denser, but scales exponentially with the
        dimension.
    random_restarts : int, optional
        The number of random initializations.
    processes : int or None, optional
        Number of processes to use when finding Nash equilibria. If 0 (default)
        run nash finding in the current process. This will work with any game
        but is not thread safe for the optimize method. If greater than zero or
        none, the game must be pickleable and nash finding will be run in
        `processes` processes. Passing None will use the number of current
        processors.
    min_reg : bool, optional
        If True, and no equilibria are found with the methods specified, return
        the point with the lowest empirical regret. This is ignored if
        at_least_one is True
    at_least_one : bool, optional
        If True, always return an equilibrium. This will use the fixed point
        method with increasingly smaller tolerances until an equilibrium with
        small regret is found. This may take an exceedingly long time to
        converge, so use with caution.
    **methods : {'replicator', 'optimize', 'scarf', 'fictitious'}={options}
        All methods to use can be specified as key word arguments to additional
        options for that method, e.g. mixed_nash(game,
        replicator={'max_iters':100}). To use the default options for a method,
        simply pass a falsey value i.e. {}, None, False. If no methods are
        specified, this will use both replicator dynamics and regret
        optimization as they tend to be reasonably fast and find different
        equilibria. Scarfs algorithm is almost never recommended to be passed
        here, as it will be called if at_least_one is True and only after
        failing with a faster method and only called once.

    Returns
    -------
    eqm : ndarray
        A two dimensional array with mixtures that have regret below
        `regret_thresh` and have norm difference of at least `dist_thresh`.
    """
    umix = game.uniform_mixture()
    utils.check(not np.isnan(game.deviation_payoffs(umix)).any(),
                'Nash finding only works on game with full deviation data')
    utils.check(processes is None or processes >= 0,
                'processes must be non-negative or None')
    utils.check(all(m in _AVAILABLE_METHODS for m in methods),
                'specified a invalid method {}', methods)

    initial_points = list(
        itertools.chain([umix], game.grid_mixtures(grid_points),
                        game.biased_mixtures(), game.role_biased_mixtures(),
                        game.random_mixtures(random_restarts)))
    equilibria = collect.mcces(dist_thresh)
    best = [np.inf, -1, None]
    chunksize = len(initial_points) if processes == 1 else 4

    # Initialize pickleable methods
    methods = methods or {'replicator': {}, 'optimize': {}}
    methods = (functools.partial(_AVAILABLE_METHODS[meth], game, **(opts
                                                                    or {}))
               for meth, opts in methods.items())

    # what to do with each candidate equilibrium
    def process(i, eqm):
        """Process an equilibrium"""
        reg = regret.mixture_regret(game, eqm)
        if reg < regret_thresh:
            equilibria.add(eqm, reg)
        best[:] = min(best, [reg, i, eqm])

    if processes == 0:
        for i, (meth,
                init) in enumerate(itertools.product(methods, initial_points)):
            process(i, meth(init))
    else:
        with multiprocessing.Pool(processes) as pool:
            for i, eqm in enumerate(
                    itertools.chain.from_iterable(
                        pool.imap_unordered(
                            m, initial_points, chunksize=chunksize)
                        for m in methods)):
                process(i, eqm)

    if equilibria:  # pylint: disable=no-else-return
        return np.array([e for e, _ in equilibria])
    elif at_least_one:
        return scarfs_algorithm(  # pylint: disable=unsubscriptable-object
            game,
            best[-1],
            regret_thresh=regret_thresh)[None]
    elif min_reg:
        return best[-1][None]  # pylint: disable=unsubscriptable-object
    else:
        return np.empty((0, game.num_strats))
Exemple #4
0
def mixed_equilibria(  # pylint: disable=too-many-locals
        game,
        style='best',
        *,
        regret_thresh=1e-2,
        dist_thresh=0.1,
        processes=None):
    """Compute mixed equilibria

    Parameters
    ----------
    game : RsGame
        Game to compute equilibria of.
    style : str, optional
        The style of equilibria funding to run. Available styles are:

        fast   - run minimal algorithms and return nothing on failure
        more   - run minimal and if nothing run other reasonable algorithms
        best   - run extra and if nothing run exponential with timeout
        one    - run extra and if nothing run exponential
        <any>* - if nothing found, return minimum regret
    regret_thresh : float, optional
        Minimum regret for a mixture to count as an equilibrium.
    dist_thresh : float, optional
        Minimum role norm for equilibria to be considered distinct. [0, 1]
    processes : int, optional
        Number of processes to compute equilibria with. If None, all available
        processes will be used.
    """
    utils.check(style in _STYLES, 'style {} not one of {}', style, _STYLES_STR)
    utils.check(processes is None or processes > 0,
                'processes must be positive or None')
    # TODO Is there a better interface for checking dev payoffs
    utils.check(
        not np.isnan(game.deviation_payoffs(game.uniform_mixture())).any(),
        'Nash finding only works on game with full deviation data')

    seq = 0
    req = 0
    best = [np.inf, 0, None]

    equilibria = collect.mcces(dist_thresh * np.sqrt(2 * game.num_roles))
    func = functools.partial(_serial_nash_func, game)
    extra = {
        'fast': lambda _, __: (),
        'more': _more,
        'best': _best,
        'one': _one,
    }[style.rstrip('*')](game, regret_thresh)

    def process_req(tup):
        """Count required methods"""
        nonlocal req
        req += 1
        return tup + (True, )

    with multiprocessing.Pool(processes) as pool:
        for preq, eqm in pool.imap_unordered(
                func,
                itertools.chain(map(process_req, _required(game)),
                                (tup + (False, ) for tup in extra))):
            seq += 1
            req -= preq
            reg = regret.mixture_regret(game, eqm)
            best[:] = min(best, [reg, seq, eqm[None]])
            if reg < regret_thresh:
                equilibria.add(eqm, reg)
            if not req and equilibria:
                return np.stack([e for e, _ in equilibria])

    assert not req
    return best[-1] if style.endswith('*') else np.empty((0, game.num_strats))
Exemple #5
0
async def inner_loop(  # pylint: disable=too-many-locals
        agame,
        *,
        initial_restrictions=None,
        regret_thresh=1e-3,
        dist_thresh=0.1,
        support_thresh=1e-4,
        restricted_game_size=3,
        num_equilibria=1,
        num_backups=1,
        devs_by_role=False,
        style='best',
        executor=None):
    """Inner loop a game using a scheduler

    Parameters
    ----------
    game : RsGame
        The game to find equilibria in. This function is most useful when game
        is a SchedulerGame, but any complete RsGame will work.
    initial_restriction : [[bool]], optional
        Initial restrictions to start inner loop from. If unspecified, every
        pure restriction is used.
    regret_thresh : float > 0, optional
        The maximum regret to consider an equilibrium an equilibrium.
    dist_thresh : float > 0, optional
        The minimum norm between two mixed profiles for them to be considered
        distinct.
    support_thresh : float > 0, optional
        Candidate equilibria strategies with probability lower than this will
        be truncated. This is useful because often Nash finding returns
        strategies with very low support, which now mean extra deviating
        strategies need to be sampled. Trimming saves these samples, but may
        increase regret above the threshold.
    restricted_game_size : int > 0, optional
        The maximum restricted game support size with which beneficial
        deviations must be explored. Restricted games with support larger than
        this are queued and only explored in the event that no equilibrium can
        be found in beneficial deviations smaller than this.
    num_equilibria : int > 0, optional
        The number of equilibria to attempt to find. Only one is guaranteed,
        but this might be beneifical if the game has a known degenerate
        equilibria, but one which is still helpful as a deviating strategy.
    num_backups : int > 0, optional
        In the event that no equilibrium can be found in beneficial deviations
        to small restricted games, other restrictions will be explored. This
        parameter indicates how many restricted games for each role should be
        explored.
    devs_by_role : boolean, optional
        If specified, deviations will only be explored for each role in order,
        proceeding to the next role only when no beneficial deviations are
        found. This can reduce the number of profiles sampled, but may also
        fail to find certain equilibria due to the different path through
        restricted games.
    style : string, optional
        A string describing the thoroughness of equilibrium finding. Seed
        `nash.mixed_equilibria` for options and a description.
    executor : Executor, optional
        The executor to be used for Nash finding. The default setting will
        allow async networking calls to continue to happen during long nash
        finding, but buy using a process pool this can take advantage of
        parallel computation.
    """
    init_role_dev = 0 if devs_by_role else None

    exp_restrictions = collect.bitset(agame.num_strats)
    backups = [[] for _ in range(agame.num_roles)]
    equilibria = collect.mcces(dist_thresh)
    loop = asyncio.get_event_loop()

    async def add_restriction(rest):
        """Adds a restriction to be evaluated"""
        if not exp_restrictions.add(rest):
            return  # already explored
        if agame.is_pure_restriction(rest):
            # Short circuit for pure restriction
            return await add_deviations(rest, rest.astype(float),
                                        init_role_dev)
        data = await agame.get_restricted_game(rest)
        reqa = await loop.run_in_executor(
            executor,
            functools.partial(nash.mixed_equilibria,
                              data,
                              regret_thresh=regret_thresh,
                              dist_thresh=dist_thresh,
                              style=style,
                              processes=1))
        if reqa.size:
            eqa = restrict.translate(
                data.trim_mixture_support(reqa, thresh=support_thresh), rest)
            await asyncio.gather(
                *[add_deviations(rest, eqm, init_role_dev) for eqm in eqa])
        else:
            logging.warning(
                "couldn't find equilibria in %s with restriction %s. This is "
                'likely due to high variance in payoffs which means '
                'quiesce should be re-run with more samples per profile. '
                'This could also be fixed by performing a more expensive '
                'equilibria search to always return one.', agame,
                agame.restriction_to_repr(rest))

    async def add_deviations(rest, mix, role_index):
        """Add deviations to be evaluated"""
        # We need the restriction here, since trimming support may increase
        # regret of strategies in the initial restriction
        data = await agame.get_deviation_game(mix > 0, role_index)
        devs = data.deviation_payoffs(mix)
        exp = np.add.reduceat(devs * mix, agame.role_starts)
        gains = devs - exp.repeat(agame.num_role_strats)
        if role_index is None:
            if np.all((gains <= regret_thresh) | rest):
                # Found equilibrium
                reg = gains.max()
                if equilibria.add(mix, reg):
                    logging.warning(
                        'found equilibrium %s in game %s with regret %f',
                        agame.mixture_to_repr(mix), agame, reg)
            else:
                await asyncio.gather(*[
                    queue_restrictions(rgains, ri, rest)
                    for ri, rgains in enumerate(
                        np.split(gains, agame.role_starts[1:]))
                ])

        else:  # Set role index
            rgains = np.split(gains, agame.role_starts[1:])[role_index]
            rrest = np.split(rest, agame.role_starts[1:])[role_index]
            if np.all((rgains <= regret_thresh) | rrest):  # No deviations
                role_index += 1
                if role_index < agame.num_roles:  # Explore next deviation
                    await add_deviations(rest, mix, role_index)
                else:  # found equilibrium
                    # This should not require scheduling as to get here all
                    # deviations have to be scheduled
                    data = await agame.get_deviation_game(mix > 0)
                    reg = regret.mixture_regret(data, mix)
                    if equilibria.add(mix, reg):
                        logging.warning(
                            'found equilibrium %s in game %s with regret %f',
                            agame.mixture_to_repr(mix), agame, reg)
            else:
                await queue_restrictions(rgains, role_index, rest)

    async def queue_restrictions(role_gains, role_index, rest):
        """Queue new restrictions appropriately"""
        role_rest = np.split(rest, agame.role_starts[1:])[role_index]
        if role_rest.all():
            return  # Can't deviate

        rest_size = rest.sum()
        role_start = agame.role_starts[role_index]

        best_resp = np.nanargmax(np.where(role_rest, np.nan, role_gains))
        if (role_gains[best_resp] > regret_thresh
                and rest_size < restricted_game_size):
            br_sub = rest.copy()
            br_sub[role_start + best_resp] = True
            await add_restriction(br_sub)
        else:
            best_resp = None  # Add best response to backup

        back = backups[role_index]
        for strat_ind, (gain, role) in enumerate(zip(role_gains, role_rest)):
            if strat_ind == best_resp or role or gain <= 0:
                continue
            sub = rest.copy()
            sub[role_start + strat_ind] = True
            heapq.heappush(back, (-gain, id(sub), sub))  # id for tie-breaking

    restrictions = (agame.pure_restrictions() if initial_restrictions is None
                    else np.asarray(initial_restrictions, bool))

    iteration = 0
    while (len(equilibria) < num_equilibria and
           (any(q
                for q in backups) or not next(iter(exp_restrictions)).all())):
        if iteration == 1:
            logging.warning(
                'scheduling backup restrictions in game %s. This only happens '
                'when quiesce criteria could not be met with current '
                'maximum restriction size (%d). This probably means that '
                'the maximum restriction size should be increased. If '
                'this is happening frequently, increasing the number of '
                'backups taken at a time might be desired (currently %s).',
                agame, restricted_game_size, num_backups)
        elif iteration > 1:
            logging.info('scheduling backup restrictions in game %s', agame)

        await asyncio.gather(*[add_restriction(r) for r in restrictions])

        restrictions = collect.bitset(agame.num_strats, exp_restrictions)
        for role, back in enumerate(backups):
            unscheduled = num_backups
            while unscheduled > 0 and back:
                rest = heapq.heappop(back)[-1]
                unscheduled -= restrictions.add(rest)
            for _ in range(unscheduled):
                added = False
                for mask in restrictions:
                    rmask = np.split(mask, agame.role_starts[1:])[role]
                    if rmask.all():
                        continue
                    rest = mask.copy()
                    # TODO We could randomize instead of taking the first
                    # strategy, but this would remove reproducability unless it
                    # was somehow predicated on all of the explored
                    # restrictions or something...
                    strat = np.split(rest,
                                     agame.role_starts[1:])[role].argmin()
                    rest[agame.role_starts[role] + strat] = True
                    restrictions.add(rest)
                    added = True
                    break
                if not added:
                    break
        iteration += 1

    # Return equilibria
    if equilibria:  # pylint: disable=no-else-return
        return np.stack([eqm for eqm, _ in equilibria])
    else:
        return np.empty((0, agame.num_strats))  # pragma: no cover
Exemple #6
0
def main(args):  # pylint: disable=too-many-statements,too-many-branches,too-many-locals
    """Entry point for analysis"""
    game = gamereader.load(args.input)

    if args.dpr is not None:
        red_players = game.role_from_repr(args.dpr, dtype=int)
        game = reduction.deviation_preserving.reduce_game(game, red_players)
    elif args.hr is not None:
        red_players = game.role_from_repr(args.hr, dtype=int)
        game = reduction.hierarchical.reduce_game(game, red_players)

    if args.dominance:
        domsub = dominance.iterated_elimination(game, 'strictdom')
        game = game.restrict(domsub)

    if args.restrictions:
        restrictions = restrict.maximal_restrictions(game)
    else:
        restrictions = np.ones((1, game.num_strats), bool)

    noeq_restrictions = []
    candidates = []
    for rest in restrictions:
        rgame = game.restrict(rest)
        reqa = nash.mixed_equilibria(rgame,
                                     style=args.style,
                                     regret_thresh=args.regret_thresh,
                                     dist_thresh=args.dist_thresh,
                                     processes=args.processes)
        eqa = restrict.translate(
            rgame.trim_mixture_support(reqa, thresh=args.support), rest)
        if eqa.size:
            candidates.extend(eqa)
        else:
            noeq_restrictions.append(rest)

    equilibria = collect.mcces(args.dist_thresh * np.sqrt(2 * game.num_roles))
    unconfirmed = collect.mcces(args.dist_thresh * np.sqrt(2 * game.num_roles))
    unexplored = {}
    for eqm in candidates:
        support = eqm > 0
        # FIXME This treats trimming support differently than quiesce does,
        # which means quiesce could find an equilibria, and this would fail to
        # find it.
        gains = regret.mixture_deviation_gains(game, eqm)
        role_gains = np.fmax.reduceat(gains, game.role_starts)
        gain = np.nanmax(role_gains)

        if np.isnan(gains).any() and gain <= args.regret_thresh:
            # Not fully explored but might be good
            unconfirmed.add(eqm, gain)

        elif np.any(role_gains > args.regret_thresh):
            # There are deviations, did we explore them?
            dev_inds = ([
                np.argmax(gs == mg) for gs, mg in zip(
                    np.split(gains, game.role_starts[1:]), role_gains)
            ] + game.role_starts)[role_gains > args.regret_thresh]
            for dind in dev_inds:
                devsupp = support.copy()
                devsupp[dind] = True
                if not np.all(devsupp <= restrictions, -1).any():
                    ind = restrict.to_id(game, devsupp)
                    old_info = unexplored.get(ind, (0, 0, 0, None))
                    new_info = (gains[dind], dind, old_info[2] + 1, eqm)
                    unexplored[ind] = max(new_info, old_info)

        else:
            # Equilibrium!
            equilibria.add(eqm, np.max(gains))

    # Output Game
    args.output.write('Game Analysis\n')
    args.output.write('=============\n')
    args.output.write(str(game))
    args.output.write('\n\n')
    if args.dpr is not None:
        args.output.write('With deviation preserving reduction: ')
        args.output.write(args.dpr.replace(';', ' '))
        args.output.write('\n\n')
    elif args.hr is not None:
        args.output.write('With hierarchical reduction: ')
        args.output.write(args.hr.replace(';', ' '))
        args.output.write('\n\n')
    if args.dominance:
        num = np.sum(~domsub)
        if num:
            args.output.write('Found {:d} dominated strateg{}\n'.format(
                num, 'y' if num == 1 else 'ies'))
            args.output.write(game.restriction_to_str(~domsub))
            args.output.write('\n\n')
        else:
            args.output.write('Found no dominated strategies\n\n')
    if args.restrictions:
        num = restrictions.shape[0]
        if num:
            args.output.write(
                'Found {:d} maximal complete restricted game{}\n\n'.format(
                    num, '' if num == 1 else 's'))
        else:
            args.output.write('Found no complete restricted games\n\n')
    args.output.write('\n')

    # Output social welfare
    args.output.write('Social Welfare\n')
    args.output.write('--------------\n')
    welfare, profile = regret.max_pure_social_welfare(game)
    if profile is None:
        args.output.write('There was no profile with complete payoff data\n\n')
    else:
        args.output.write('\nMaximum social welfare profile:\n')
        args.output.write(game.profile_to_str(profile))
        args.output.write('\nWelfare: {:.4f}\n\n'.format(welfare))

        if game.num_roles > 1:
            for role, welfare, profile in zip(
                    game.role_names,
                    *regret.max_pure_social_welfare(game, by_role=True)):
                args.output.write(
                    'Maximum "{}" welfare profile:\n'.format(role))
                args.output.write(game.profile_to_str(profile))
                args.output.write('\nWelfare: {:.4f}\n\n'.format(welfare))

    args.output.write('\n')

    # Output Equilibria
    args.output.write('Equilibria\n')
    args.output.write('----------\n')
    if equilibria:
        args.output.write('Found {:d} equilibri{}\n\n'.format(
            len(equilibria), 'um' if len(equilibria) == 1 else 'a'))
        for i, (eqm, reg) in enumerate(equilibria, 1):
            args.output.write('Equilibrium {:d}:\n'.format(i))
            args.output.write(game.mixture_to_str(eqm))
            args.output.write('\nRegret: {:.4f}\n\n'.format(reg))
    else:
        args.output.write('Found no equilibria\n\n')
    args.output.write('\n')

    # Output No-equilibria Subgames
    args.output.write('No-equilibria Subgames\n')
    args.output.write('----------------------\n')
    if noeq_restrictions:
        args.output.write(
            'Found {:d} no-equilibria restricted game{}\n\n'.format(
                len(noeq_restrictions),
                '' if len(noeq_restrictions) == 1 else 's'))
        noeq_restrictions.sort(key=lambda x: x.sum())
        for i, subg in enumerate(noeq_restrictions, 1):
            args.output.write(
                'No-equilibria restricted game {:d}:\n'.format(i))
            args.output.write(game.restriction_to_str(subg))
            args.output.write('\n\n')
    else:
        args.output.write('Found no no-equilibria restricted games\n\n')
    args.output.write('\n')

    # Output Unconfirmed Candidates
    args.output.write('Unconfirmed Candidate Equilibria\n')
    args.output.write('--------------------------------\n')
    if unconfirmed:
        args.output.write('Found {:d} unconfirmed candidate{}\n\n'.format(
            len(unconfirmed), '' if len(unconfirmed) == 1 else 's'))
        ordered = sorted((sum(e > 0 for e in m), r, m) for m, r in unconfirmed)
        for i, (_, reg_bound, eqm) in enumerate(ordered, 1):
            args.output.write('Unconfirmed candidate {:d}:\n'.format(i))
            args.output.write(game.mixture_to_str(eqm))
            args.output.write(
                '\nRegret at least: {:.4f}\n\n'.format(reg_bound))
    else:
        args.output.write('Found no unconfirmed candidate equilibria\n\n')
    args.output.write('\n')

    # Output Unexplored Subgames
    args.output.write('Unexplored Best-response Subgames\n')
    args.output.write('---------------------------------\n')
    if unexplored:
        min_supp = min(restrict.from_id(game, sid).sum() for sid in unexplored)
        args.output.write(
            'Found {:d} unexplored best-response restricted game{}\n'.format(
                len(unexplored), '' if len(unexplored) == 1 else 's'))
        args.output.write(
            'Smallest unexplored restricted game has support {:d}\n\n'.format(
                min_supp))

        ordered = sorted((
            restrict.from_id(game, sind).sum(),
            -gain,
            dev,
            restrict.from_id(game, sind),
            eqm,
        ) for sind, (gain, dev, _, eqm) in unexplored.items())
        for i, (_, ngain, dev, sub, eqm) in enumerate(ordered, 1):
            args.output.write('Unexplored restricted game {:d}:\n'.format(i))
            args.output.write(game.restriction_to_str(sub))
            args.output.write('\n{:.4f} for deviating to {} from:\n'.format(
                -ngain, game.strat_name(dev)))
            args.output.write(game.mixture_to_str(eqm))
            args.output.write('\n\n')
    else:
        args.output.write(
            'Found no unexplored best-response restricted games\n\n')
    args.output.write('\n')

    # Output json data
    args.output.write('Json Data\n')
    args.output.write('=========\n')
    json_data = {
        'equilibria': [game.mixture_to_json(eqm) for eqm, _ in equilibria]
    }
    json.dump(json_data, args.output)
    args.output.write('\n')
Exemple #7
0
def main(args): # pylint: disable=too-many-statements,too-many-branches,too-many-locals
    """Entry point for analysis"""
    game = gamereader.load(args.input)

    if args.dpr is not None:
        red_players = game.role_from_repr(args.dpr, dtype=int)
        game = reduction.deviation_preserving.reduce_game(game, red_players)
    elif args.hr is not None:
        red_players = game.role_from_repr(args.hr, dtype=int)
        game = reduction.hierarchical.reduce_game(game, red_players)

    if args.dominance:
        domsub = dominance.iterated_elimination(game, 'strictdom')
        game = game.restrict(domsub)

    if args.restrictions:
        restrictions = restrict.maximal_restrictions(game)
    else:
        restrictions = np.ones((1, game.num_strats), bool)

    noeq_restrictions = []
    candidates = []
    for rest in restrictions:
        rgame = game.restrict(rest)
        reqa = nash.mixed_equilibria(
            rgame, style=args.style, regret_thresh=args.regret_thresh,
            dist_thresh=args.dist_thresh, processes=args.processes)
        eqa = restrict.translate(rgame.trim_mixture_support(
            reqa, thresh=args.support), rest)
        if eqa.size:
            candidates.extend(eqa)
        else:
            noeq_restrictions.append(rest)

    equilibria = collect.mcces(args.dist_thresh * np.sqrt(2 * game.num_roles))
    unconfirmed = collect.mcces(args.dist_thresh * np.sqrt(2 * game.num_roles))
    unexplored = {}
    for eqm in candidates:
        support = eqm > 0
        # FIXME This treats trimming support differently than quiesce does,
        # which means quiesce could find an equilibria, and this would fail to
        # find it.
        gains = regret.mixture_deviation_gains(game, eqm)
        role_gains = np.fmax.reduceat(gains, game.role_starts)
        gain = np.nanmax(role_gains)

        if np.isnan(gains).any() and gain <= args.regret_thresh:
            # Not fully explored but might be good
            unconfirmed.add(eqm, gain)

        elif np.any(role_gains > args.regret_thresh):
            # There are deviations, did we explore them?
            dev_inds = ([np.argmax(gs == mg) for gs, mg
                         in zip(np.split(gains, game.role_starts[1:]),
                                role_gains)] +
                        game.role_starts)[role_gains > args.regret_thresh]
            for dind in dev_inds:
                devsupp = support.copy()
                devsupp[dind] = True
                if not np.all(devsupp <= restrictions, -1).any():
                    ind = restrict.to_id(game, devsupp)
                    old_info = unexplored.get(ind, (0, 0, 0, None))
                    new_info = (gains[dind], dind, old_info[2] + 1, eqm)
                    unexplored[ind] = max(new_info, old_info)

        else:
            # Equilibrium!
            equilibria.add(eqm, np.max(gains))

    # Output Game
    args.output.write('Game Analysis\n')
    args.output.write('=============\n')
    args.output.write(str(game))
    args.output.write('\n\n')
    if args.dpr is not None:
        args.output.write('With deviation preserving reduction: ')
        args.output.write(args.dpr.replace(';', ' '))
        args.output.write('\n\n')
    elif args.hr is not None:
        args.output.write('With hierarchical reduction: ')
        args.output.write(args.hr.replace(';', ' '))
        args.output.write('\n\n')
    if args.dominance:
        num = np.sum(~domsub)
        if num:
            args.output.write('Found {:d} dominated strateg{}\n'.format(
                num, 'y' if num == 1 else 'ies'))
            args.output.write(game.restriction_to_str(~domsub))
            args.output.write('\n\n')
        else:
            args.output.write('Found no dominated strategies\n\n')
    if args.restrictions:
        num = restrictions.shape[0]
        if num:
            args.output.write(
                'Found {:d} maximal complete restricted game{}\n\n'.format(
                    num, '' if num == 1 else 's'))
        else:
            args.output.write('Found no complete restricted games\n\n')
    args.output.write('\n')

    # Output social welfare
    args.output.write('Social Welfare\n')
    args.output.write('--------------\n')
    welfare, profile = regret.max_pure_social_welfare(game)
    if profile is None:
        args.output.write('There was no profile with complete payoff data\n\n')
    else:
        args.output.write('\nMaximum social welfare profile:\n')
        args.output.write(game.profile_to_str(profile))
        args.output.write('\nWelfare: {:.4f}\n\n'.format(welfare))

        if game.num_roles > 1:
            for role, welfare, profile in zip(
                    game.role_names,
                    *regret.max_pure_social_welfare(game, by_role=True)):
                args.output.write('Maximum "{}" welfare profile:\n'.format(
                    role))
                args.output.write(game.profile_to_str(profile))
                args.output.write('\nWelfare: {:.4f}\n\n'.format(welfare))

    args.output.write('\n')

    # Output Equilibria
    args.output.write('Equilibria\n')
    args.output.write('----------\n')
    if equilibria:
        args.output.write('Found {:d} equilibri{}\n\n'.format(
            len(equilibria), 'um' if len(equilibria) == 1 else 'a'))
        for i, (eqm, reg) in enumerate(equilibria, 1):
            args.output.write('Equilibrium {:d}:\n'.format(i))
            args.output.write(game.mixture_to_str(eqm))
            args.output.write('\nRegret: {:.4f}\n\n'.format(reg))
    else:
        args.output.write('Found no equilibria\n\n')
    args.output.write('\n')

    # Output No-equilibria Subgames
    args.output.write('No-equilibria Subgames\n')
    args.output.write('----------------------\n')
    if noeq_restrictions:
        args.output.write(
            'Found {:d} no-equilibria restricted game{}\n\n'.format(
                len(noeq_restrictions),
                '' if len(noeq_restrictions) == 1 else 's'))
        noeq_restrictions.sort(key=lambda x: x.sum())
        for i, subg in enumerate(noeq_restrictions, 1):
            args.output.write(
                'No-equilibria restricted game {:d}:\n'.format(i))
            args.output.write(game.restriction_to_str(subg))
            args.output.write('\n\n')
    else:
        args.output.write('Found no no-equilibria restricted games\n\n')
    args.output.write('\n')

    # Output Unconfirmed Candidates
    args.output.write('Unconfirmed Candidate Equilibria\n')
    args.output.write('--------------------------------\n')
    if unconfirmed:
        args.output.write('Found {:d} unconfirmed candidate{}\n\n'.format(
            len(unconfirmed), '' if len(unconfirmed) == 1 else 's'))
        ordered = sorted(
            (sum(e > 0 for e in m), r, m) for m, r in unconfirmed)
        for i, (_, reg_bound, eqm) in enumerate(ordered, 1):
            args.output.write('Unconfirmed candidate {:d}:\n'.format(i))
            args.output.write(game.mixture_to_str(eqm))
            args.output.write('\nRegret at least: {:.4f}\n\n'.format(
                reg_bound))
    else:
        args.output.write('Found no unconfirmed candidate equilibria\n\n')
    args.output.write('\n')

    # Output Unexplored Subgames
    args.output.write('Unexplored Best-response Subgames\n')
    args.output.write('---------------------------------\n')
    if unexplored:
        min_supp = min(restrict.from_id(game, sid).sum() for sid in unexplored)
        args.output.write(
            'Found {:d} unexplored best-response restricted game{}\n'.format(
                len(unexplored), '' if len(unexplored) == 1 else 's'))
        args.output.write(
            'Smallest unexplored restricted game has support {:d}\n\n'.format(
                min_supp))

        ordered = sorted((
            restrict.from_id(game, sind).sum(),
            -gain, dev,
            restrict.from_id(game, sind),
            eqm,
        ) for sind, (gain, dev, _, eqm) in unexplored.items())
        for i, (_, ngain, dev, sub, eqm) in enumerate(ordered, 1):
            args.output.write('Unexplored restricted game {:d}:\n'.format(i))
            args.output.write(game.restriction_to_str(sub))
            args.output.write('\n{:.4f} for deviating to {} from:\n'.format(
                -ngain, game.strat_name(dev)))
            args.output.write(game.mixture_to_str(eqm))
            args.output.write('\n\n')
    else:
        args.output.write(
            'Found no unexplored best-response restricted games\n\n')
    args.output.write('\n')

    # Output json data
    args.output.write('Json Data\n')
    args.output.write('=========\n')
    json_data = {
        'equilibria': [game.mixture_to_json(eqm) for eqm, _ in equilibria]}
    json.dump(json_data, args.output)
    args.output.write('\n')
Exemple #8
0
def mixed_nash( # pylint: disable=too-many-locals
        game, *, regret_thresh=1e-3, dist_thresh=0.1, grid_points=2,
        random_restarts=0, processes=0, min_reg=False, at_least_one=False,
        **methods):
    """Finds role-symmetric mixed Nash equilibria

    This is the intended front end for nash equilibria finding, wrapping the
    individual methods in a convenient front end that also support parallel
    execution. Scipy optimize, and hence nash finding with the optimize method
    is NOT thread safe. This can be mitigated by running nash finding in a
    separate process (by setting processes > 0) if the game is pickleable.

    This is the old style nash finding and provides more options. For new
    methods, mixture_equilibria is the preferred interface.

    Arguments
    ---------
    regret_thresh : float, optional
        The threshold to consider an equilibrium found.
    dist_thresh : float, optional
        The threshold for considering equilibria distinct.
    grid_points : int > 1, optional
        The number of grid points to use for mixture seeds. two implies just
        pure mixtures, more will be denser, but scales exponentially with the
        dimension.
    random_restarts : int, optional
        The number of random initializations.
    processes : int or None, optional
        Number of processes to use when finding Nash equilibria. If 0 (default)
        run nash finding in the current process. This will work with any game
        but is not thread safe for the optimize method. If greater than zero or
        none, the game must be pickleable and nash finding will be run in
        `processes` processes. Passing None will use the number of current
        processors.
    min_reg : bool, optional
        If True, and no equilibria are found with the methods specified, return
        the point with the lowest empirical regret. This is ignored if
        at_least_one is True
    at_least_one : bool, optional
        If True, always return an equilibrium. This will use the fixed point
        method with increasingly smaller tolerances until an equilibrium with
        small regret is found. This may take an exceedingly long time to
        converge, so use with caution.
    **methods : {'replicator', 'optimize', 'scarf', 'fictitious'}={options}
        All methods to use can be specified as key word arguments to additional
        options for that method, e.g. mixed_nash(game,
        replicator={'max_iters':100}). To use the default options for a method,
        simply pass a falsey value i.e. {}, None, False. If no methods are
        specified, this will use both replicator dynamics and regret
        optimization as they tend to be reasonably fast and find different
        equilibria. Scarfs algorithm is almost never recommended to be passed
        here, as it will be called if at_least_one is True and only after
        failing with a faster method and only called once.

    Returns
    -------
    eqm : ndarray
        A two dimensional array with mixtures that have regret below
        `regret_thresh` and have norm difference of at least `dist_thresh`.
    """
    umix = game.uniform_mixture()
    utils.check(
        not np.isnan(game.deviation_payoffs(umix)).any(),
        'Nash finding only works on game with full deviation data')
    utils.check(
        processes is None or processes >= 0,
        'processes must be non-negative or None')
    utils.check(
        all(m in _AVAILABLE_METHODS for m in methods),
        'specified a invalid method {}', methods)

    initial_points = list(itertools.chain(
        [umix],
        game.grid_mixtures(grid_points),
        game.biased_mixtures(),
        game.role_biased_mixtures(),
        game.random_mixtures(random_restarts)))
    equilibria = collect.mcces(dist_thresh)
    best = [np.inf, -1, None]
    chunksize = len(initial_points) if processes == 1 else 4

    # Initialize pickleable methods
    methods = methods or {'replicator': {}, 'optimize': {}}
    methods = (
        functools.partial(_AVAILABLE_METHODS[meth], game, **(opts or {}))
        for meth, opts in methods.items())

    # what to do with each candidate equilibrium
    def process(i, eqm):
        """Process an equilibrium"""
        reg = regret.mixture_regret(game, eqm)
        if reg < regret_thresh:
            equilibria.add(eqm, reg)
        best[:] = min(best, [reg, i, eqm])

    if processes == 0:
        for i, (meth, init) in enumerate(itertools.product(
                methods, initial_points)):
            process(i, meth(init))
    else:
        with multiprocessing.Pool(processes) as pool:
            for i, eqm in enumerate(itertools.chain.from_iterable(
                    pool.imap_unordered(m, initial_points, chunksize=chunksize)
                    for m in methods)):
                process(i, eqm)

    if equilibria: # pylint: disable=no-else-return
        return np.array([e for e, _ in equilibria])
    elif at_least_one:
        return scarfs_algorithm( # pylint: disable=unsubscriptable-object
            game, best[-1], regret_thresh=regret_thresh)[None]
    elif min_reg:
        return best[-1][None] # pylint: disable=unsubscriptable-object
    else:
        return np.empty((0, game.num_strats))
Exemple #9
0
def mixed_equilibria( # pylint: disable=too-many-locals
        game, style='best', *, regret_thresh=1e-2, dist_thresh=0.1,
        processes=None):
    """Compute mixed equilibria

    Parameters
    ----------
    game : RsGame
        Game to compute equilibria of.
    style : str, optional
        The style of equilibria funding to run. Available styles are:

        fast   - run minimal algorithms and return nothing on failure
        more   - run minimal and if nothing run other reasonable algorithms
        best   - run extra and if nothing run exponential with timeout
        one    - run extra and if nothing run exponential
        <any>* - if nothing found, return minimum regret
    regret_thresh : float, optional
        Minimum regret for a mixture to count as an equilibrium.
    dist_thresh : float, optional
        Minimum role norm for equilibria to be considered distinct. [0, 1]
    processes : int, optional
        Number of processes to compute equilibria with. If None, all available
        processes will be used.
    """
    utils.check(style in _STYLES, 'style {} not one of {}', style, _STYLES_STR)
    utils.check(
        processes is None or processes > 0,
        'processes must be positive or None')
    # TODO Is there a better interface for checking dev payoffs
    utils.check(
        not np.isnan(game.deviation_payoffs(game.uniform_mixture())).any(),
        'Nash finding only works on game with full deviation data')

    seq = 0
    req = 0
    best = [np.inf, 0, None]

    equilibria = collect.mcces(dist_thresh * np.sqrt(2 * game.num_roles))
    func = functools.partial(_serial_nash_func, game)
    extra = {
        'fast': lambda _, __: (),
        'more': _more,
        'best': _best,
        'one': _one,
    }[style.rstrip('*')](game, regret_thresh)

    def process_req(tup):
        """Count required methods"""
        nonlocal req
        req += 1
        return tup + (True,)

    with multiprocessing.Pool(processes) as pool:
        for preq, eqm in pool.imap_unordered(func, itertools.chain(
                map(process_req, _required(game)),
                (tup + (False,) for tup in extra))):
            seq += 1
            req -= preq
            reg = regret.mixture_regret(game, eqm)
            best[:] = min(best, [reg, seq, eqm[None]])
            if reg < regret_thresh:
                equilibria.add(eqm, reg)
            if not req and equilibria:
                return np.stack([e for e, _ in equilibria])

    assert not req
    return best[-1] if style.endswith('*') else np.empty((0, game.num_strats))