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
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
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))
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))
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
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')
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')
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))
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))