def test_pure_subgame(players, strategies): game = rsgame.BaseGame(players, strategies) subgames = subgame.pure_subgames(game) expectation = game.num_strategies[None].repeat(game.num_roles, 0) np.fill_diagonal(expectation, 1) expectation = game.role_repeat(expectation.prod(1)) assert np.all(subgames.sum(0) == expectation)
def test_num_subgames(): game = rsgame.BaseGame([3, 4], [4, 3]) actual = subgame.num_pure_subgames(game) expected = subgame.pure_subgames(game).shape[0] assert actual == 12 == expected actual = subgame.num_all_subgames(game) assert actual == 105
def quiesce(sim, game, serial, base_name, configuration={}, dpr=None, log=logging, profiles=(), all_devs=True, max_profiles=500, max_subgame_size=3, sleep_time=300, required_equilibria=1, regret_thresh=1e-3, reschedule_limit=10, process_memory=4096, observation_time=600, observation_increment=1, nodes=1): """Quiesce a game""" # Create scheduler sched = sim.create_generic_scheduler( name='{base}_generic_quiesce_{random}'.format( base=base_name, random=utils.random_string(6)), active=1, process_memory=process_memory, size=game.num_players.sum(), time_per_observation=observation_time, observations_per_simulation=observation_increment, nodes=nodes, default_observation_requirement=observation_increment, configuration=configuration) # Add roles and counts to scheduler for role, count in zip(serial.role_names, game.num_players): sched.add_role(role, count) log.info('Created scheduler %d ' '(http://egtaonline.eecs.umich.edu/generic_schedulers/%d)', sched.id, sched.id) # Data lookup psched = profsched.ProfileScheduler( game, serial, sched, max_profiles, log, profiles) # Set up reduction if dpr is None: red = reduction.Identity(game.num_strategies, game.num_players) else: red = reduction.DeviationPreserving(game.num_strategies, game.num_players, dpr) # Set up main scheduler abstraction qsched = profsched.QuiesceScheduler(game, red, psched) confirmed_equilibria = [] # Confirmed equilibra explored_subgames = [] # Already explored subgames explored_mixtures = [] # Already explored mixtures backup = [] # Extra subgames to explore subgames = [] # Subgames that are scheduling deviations = [] # Deviations that are scheduling # Define useful functions def add_subgame(subm): """Adds a subgame to the scheduler""" if not any(np.all(subm <= s) for s in explored_subgames): # Unexplored explored_subgames[:] = [s for s in explored_subgames if np.any(s > subm)] explored_subgames.append(subm) log.debug('Exploring subgame:\n%s\n', json.dumps( {r: list(s) for r, s in serial.to_prof_json(subm).items()}, indent=2)) subgames.append( qsched.schedule_subgame(subm, observation_increment)) else: # Subgame already explored log.debug('Subgame already explored:\n%s\n', json.dumps( {r: list(s) for r, s in serial.to_prof_json(subm).items()}, indent=2)) def add_mixture(mixture, role_index=None): """Adds the given mixture to the scheduler""" if any(linalg.norm(mix - mixture) < 1e-3 and ( role_index is None or role_index <= ri) for mix, ri in explored_mixtures): if role_index is None: log.debug('Mixture already explored:\n%s\n', json.dumps( serial.to_prof_json(mixture), indent=2)) else: log.debug('Mixture already explored for role "%s":\n%s\n', serial.role_names[role_index], json.dumps(serial.to_prof_json(mixture), indent=2)) else: explored_mixtures.append((mixture, role_index)) if role_index is None: log.debug('Exploring equilibrium deviations:\n%s\n', json.dumps(serial.to_prof_json(mixture), indent=2)) else: log.debug( 'Exploring equilibrium deviations for role "%s":\n%s\n', serial.role_names[role_index], json.dumps(serial.to_prof_json(mixture), indent=2)) dev = qsched.schedule_deviations( mixture > 0, observation_increment, role_index) deviations.append((mixture, dev)) def analyze_subgame(unsched_subgames, sub): """Process a subgame""" if sub.is_complete(): subg = sub.get_subgame() sub_eqa = nash.mixed_nash(subg, regret_thresh=regret_thresh) eqa = subgame.translate(subg.trim_mixture_support(sub_eqa), sub.subgame_mask) if eqa.size == 0: # No equilibria if sub.counts < reschedule_limit * observation_increment: log.info( 'Found no equilibria in subgame:\n%s\n', json.dumps( {r: list(s) for r, s in serial.to_prof_json(sub.subgame_mask).items()}, indent=2)) sub.update_counts(sub.counts + observation_increment) unsched_subgames.append(sub) else: log.error( 'Failed to find equilibria in subgame:\n%s\n', json.dumps( {r: list(s) for r, s in serial.to_prof_json(subm).items()}, indent=2)) else: log.debug( 'Found candidate equilibria:\n%s\nin subgame:\n%s\n', json.dumps(list(map(serial.to_prof_json, eqa)), indent=2), json.dumps( {r: list(s) for r, s in serial.to_prof_json(sub.subgame_mask).items()}, indent=2)) if all_devs: for eqm in eqa: add_mixture(eqm) else: for eqm in eqa: add_mixture(eqm, 0) else: unsched_subgames.append(sub) if all_devs: def analyze_deviations(unsched_deviations, mix, dev): """Analyzes responses to an equilibrium and book keeps""" if dev.is_complete(): dev_game = dev.get_game() responses = regret.mixture_deviation_gains( dev_game, mix, assume_complete=True) log.debug('Responses:\n%s\nto candidate equilibrium:\n%s\n', json.dumps(serial.to_prof_json( responses, filter_zeros=False), indent=2), json.dumps(serial.to_prof_json(mix), indent=2)) if np.all(responses < regret_thresh): # found equilibria if not any(linalg.norm(m - mix) < 1e-3 for m in confirmed_equilibria): confirmed_equilibria.append(mix) log.info('Confirmed equilibrium:\n%s\n', json.dumps( serial.to_prof_json(mix), indent=2)) else: # Queue up next subgames subsize = dev.subgame_mask.sum() # TODO Normalize role deviations for rstart, role_resps in zip(game.role_starts, game.role_split(responses)): order = np.argpartition( role_resps, role_resps.size - 1) gain = role_resps[order[-1]] if gain > 0: # Positive best response exists for role subm = dev.subgame_mask.copy() subm[order[-1] + rstart] = True if subsize < max_subgame_size: add_subgame(subm) else: heapq.heappush(backup, ( (False, False, subsize, -gain), subm)) order = order[:-1] # Priority for backup is (not best response, not # beneficial response, subgame size, deviation loss). # Thus, best responses are first, then positive # responses, then small subgames, then highest gain. # Add the rest to the backup for ind in order: subm = dev.subgame_mask.copy() subm[ind + rstart] = True gain = role_resps[ind] heapq.heappush(backup, ( (True, gain < 0, subsize, -gain, id(subm)), subm)) else: unsched_deviations.append((mix, dev)) else: def analyze_deviations(unsched_deviations, mix, dev): """Analyzes responses to an equilibrium and book keeps""" if dev.is_complete(): dev_game = dev.get_game() role_resps = game.role_split(regret.mixture_deviation_gains( dev_game, mix, assume_complete=True))[dev.role_index] log.debug( '"%s" Responses:\n%s\nto candidate equilibrium:\n%s\n', serial.role_names[dev.role_index], json.dumps(dict(zip(serial.strat_names[dev.role_index], role_resps)), indent=2), json.dumps(serial.to_prof_json(mix), indent=2)) if np.all(role_resps < regret_thresh): # role has no deviations if dev.role_index == game.num_roles - 1: if not any(linalg.norm(m - mix) < 1e-3 for m in confirmed_equilibria): confirmed_equilibria.append(mix) log.info('Confirmed equilibrium:\n%s\n', json.dumps(serial.to_prof_json(mix), indent=2)) else: add_mixture(mix, dev.role_index + 1) else: # Queue up next subgames subsize = dev.subgame_mask.sum() # TODO Normalize role deviations rstart = game.role_starts[dev.role_index] order = np.argpartition(role_resps, role_resps.size - 1) gain = role_resps[order[-1]] # Positive best response exists for role subm = dev.subgame_mask.copy() subm[order[-1] + rstart] = True if subsize < max_subgame_size: add_subgame(subm) else: heapq.heappush(backup, ( (False, False, subsize, -gain), subm)) # Priority for backup is (not best response, not beneficial # response, subgame size, deviation loss). Thus, best # responses are first, then positive responses, then small # subgames, then highest gain. # Add the rest to the backup for ind in order[:-1]: subm = dev.subgame_mask.copy() subm[ind + rstart] = True gain = role_resps[ind] heapq.heappush(backup, ( (True, gain < 0, subsize, -gain, id(subm)), subm)) else: unsched_deviations.append((mix, dev)) try: # Initialize with pure subgames for subm in subgame.pure_subgames(game): add_subgame(subm) # While still scheduling left to do while subgames or deviations: if (qsched.update() or any(s.is_complete() for s in subgames) or any(d.is_complete() for _, d in deviations)): # Something finished scheduling unsched_subgames = [] for sub in subgames: analyze_subgame(unsched_subgames, sub) subgames = unsched_subgames unsched_deviations = [] for mix, dev in deviations: analyze_deviations(unsched_deviations, mix, dev) deviations = unsched_deviations if (not subgames and not deviations and len(confirmed_equilibria) < required_equilibria): # We've finished all the required stuff, but still haven't # found an equilibrium, so pop a backup off log.debug('Extracting backup game\n') while backup and not subgames: add_subgame(heapq.heappop(backup)[1]) else: # We're still waiting for jobs to complete, so take a break log.debug('Waiting %d seconds for simulations to finish...\n', sleep_time) time.sleep(sleep_time) except KeyboardInterrupt: # Manually killed, so just deactivate log.error('Manually killed quiesce script. Deactivating scheduler\n') sched.deactivate() sys.exit(1) log.info('Deactivating scheduler %d\n', sched.id) sched.deactivate() final_game = psched.get_game() red_game = red.reduce_game(final_game, True) equilibria = (np.array(confirmed_equilibria) if confirmed_equilibria else np.empty((0, game.num_role_strats))) complete_subgames = np.array(explored_subgames) regrets = np.fromiter((regret.mixture_regret(red_game, eqm) for eqm in confirmed_equilibria), float, len(confirmed_equilibria)) final_log = [dict(regret=float(r), equilibrium=serial.to_prof_json(eqm)) for eqm, r in zip(equilibria, regrets)] log.error('Finished quiescing\nConfirmed equilibria:\n%s\n' 'Explored %d subgames, sampled %d profiles with %d distinct\n', json.dumps(final_log, indent=2), complete_subgames.shape[0], psched.num_profiles, psched.num_unique_profiles) # TODO return failed subgames return equilibria, complete_subgames, final_game