def test_random_trace_equilibria(base): """Test random equilibrium trace""" game0 = gamegen.poly_aggfn(base.num_role_players, base.num_role_strats, 6) game1 = gamegen.poly_aggfn(base.num_role_players, base.num_role_strats, 6) eqa = game0.trim_mixture_support(nash.mixed_nash( game0, regret_thresh=1e-4)) for eqm in eqa: if regret.mixture_regret(game0, eqm) > 1e-3: # trimmed equilibrium had too high of regret... continue # pragma: no cover probs, mixes = trace.trace_equilibrium(game0, game1, 0, eqm, 1) for prob, mix in zip(probs, mixes): reg = regret.mixture_regret(rsgame.mix(game0, game1, prob), mix) assert reg <= 1.1e-3 eqa = game1.trim_mixture_support(nash.mixed_nash( game1, regret_thresh=1e-4)) for eqm in eqa: if regret.mixture_regret(game1, eqm) > 1e-3: # trimmed equilibrium had too high of regret... continue # pragma: no cover probs, mixes = trace.trace_equilibrium(game0, game1, 1, eqm, 0) for prob, mix in zip(probs, mixes): reg = regret.mixture_regret(rsgame.mix(game0, game1, prob), mix) assert reg <= 1.1e-3
def trace_interpolate(game0, game1, peqs, eqa, targets, **kwargs): # pylint: disable=too-many-locals """Get an equilibrium at a specific time Parameters ---------- game0 : RsGame The game to get data from when the mixture probability is 0. game1 : RsGame The game to get data from when the mixture probability is 1. peqs : [float] A parallel list of probabilities for each equilibria in a continuous trace. eqa : [eqm] A parallel list of equilibria for each probability representing continuous equilibria for prob mixture games. targets : [float] The probabilities to compute an equilibria at. kwargs : options The same options as `trace_equilibrium`. """ peqs = np.asarray(peqs, float) eqa = np.asarray(eqa, float) targets = np.asarray(targets, float) # Make everything sorted if np.all(np.diff(peqs) <= 0): peqs = peqs[::-1] eqa = eqa[::-1] order = np.argsort(targets) targets = targets[order] utils.check(np.all(np.diff(peqs) >= 0), 'trace probabilities must be sorted') utils.check(peqs[0] <= targets[0] and targets[-1] <= peqs[-1], 'targets must be internal to trace') result = np.empty((targets.size, game0.num_strats)) scan = zip(utils.subsequences(peqs), utils.subsequences(eqa)) (pi1, pi2), (eqm1, eqm2) = next(scan) for target, i in zip(targets, order): while target > pi2: (pi1, pi2), (eqm1, eqm2) = next(scan) (*_, pt1), (*_, eqt1) = trace_equilibrium( # pylint: disable=too-many-star-expressions game0, game1, pi1, eqm1, target, **kwargs) (*_, pt2), (*_, eqt2) = trace_equilibrium( # pylint: disable=too-many-star-expressions game0, game1, pi2, eqm2, target, **kwargs) if np.isclose(pt1, target) and np.isclose(pt2, target): mixgame = rsgame.mix(game0, game1, target) _, _, result[i] = min( (regret.mixture_regret(mixgame, eqt1), 0, eqt1), (regret.mixture_regret(mixgame, eqt2), 1, eqt2)) elif np.isclose(pt1, target): result[i] = eqt1 elif np.isclose(pt2, target): result[i] = eqt2 else: # pragma: no cover raise ValueError('ode solving failed to reach prob') return result
def test_random_trace_interpolate(game0, game1): # pylint: disable=too-many-locals """Test random trace interpolation""" prob = np.random.random() eqa = game0.trim_mixture_support(nash.mixed_nash( rsgame.mix(game0, game1, prob), regret_thresh=1e-4)) for eqm in eqa: if regret.mixture_regret(rsgame.mix(game0, game1, prob), eqm) > 1e-3: # trimmed equilibrium had too high of regret... continue # pragma: no cover for target in [0, 1]: # Test that interpolate recovers missing equilibria probs, mixes = trace.trace_equilibrium( game0, game1, prob, eqm, target) if probs.size < 3: # not enough to test leave one out continue # pragma: no cover start, interp, end = np.sort(np.random.choice( probs.size, 3, replace=False)) interp_mix, = trace.trace_interpolate( game0, game1, [probs[start], probs[end]], [mixes[start], mixes[end]], [probs[interp]]) assert np.allclose(interp_mix, mixes[interp], rtol=1e-2, atol=2e-2) # Test interp at first mix, = trace.trace_interpolate( game0, game1, probs, mixes, [probs[0]]) assert np.allclose(mix, mixes[0], rtol=1e-2, atol=2e-2) # Test interp at last mix, = trace.trace_interpolate( game0, game1, probs, mixes, [probs[-1]]) assert np.allclose(mix, mixes[-1], rtol=1e-2, atol=2e-2) # Test random t p_interp = np.random.uniform(probs[0], probs[-1]) mix, = trace.trace_interpolate( game0, game1, probs, mixes, [p_interp]) assert regret.mixture_regret(rsgame.mix( game0, game1, p_interp), mix) <= 1.1e-3
async def get_point(prob, eqm): """Get the point in a trace for an equilibrium""" supp = eqm > 0 game0 = await agame0.get_deviation_game(supp) game1 = await agame1.get_deviation_game(supp) reg = regret.mixture_regret(rsgame.mix(game0, game1, prob), eqm) return { "t": float(prob), "equilibrium": sched0.mixture_to_json(eqm), "regret": float(reg), }
def _smooth_trace(game0, game1, probs, eqa, trace_args): """Smooth the equilibria in a trace in place Smoothing attempts to trace out from one time to an adjacent time. If the new point has lower regret, it's taken instead. This onle goes one direction, so it should be repeated for reversed views. """ for (pfrom, pto), (eqmfrom, eqmto) in zip(utils.subsequences(probs), utils.subsequences(eqa)): (*_, pres), (*_, eqmres) = trace.trace_equilibrium( # pylint: disable=too-many-star-expressions game0, game1, pfrom, eqmfrom, pto, **trace_args) if np.isclose(pres, pto): mixgame = rsgame.mix(game0, game1, pto) regto = regret.mixture_regret(mixgame, eqmto) regres = regret.mixture_regret(mixgame, eqmres) if regres < regto: np.copyto(eqmto, eqmres)
async def test_mix_asyncgame(): """Test that that mixture async games work""" game0 = gamegen.game([4, 3], [3, 4]) game1 = gamegen.game([4, 3], [3, 4]) agame = asyncgame.mix(asyncgame.wrap(game0), asyncgame.wrap(game1), 0.4) assert agame.get_game() == rsgame.mix(game0, game1, 0.4) assert str(agame) == "{} - 0.4 - {}".format(repr(game0), repr(game1)) rest = agame.random_restriction() rgame = await agame.get_restricted_game(rest) assert rgame.is_complete() assert rsgame.empty_copy(rgame) == rsgame.empty_copy(game0.restrict(rest)) dgame = await agame.get_deviation_game(rest) mix = restrict.translate(rgame.random_mixture(), rest) assert not np.isnan(dgame.deviation_payoffs(mix)).any() dup = asyncgame.mix(asyncgame.wrap(game0), asyncgame.wrap(game1), 0.4) assert hash(dup) == hash(agame) assert dup == agame
async def get_deviation_game(self, rest, role_index=None): game0, game1 = await asyncio.gather( self._agame0.get_deviation_game(rest, role_index), self._agame1.get_deviation_game(rest, role_index), ) return rsgame.mix(game0, game1, self._prob)
async def get_restricted_game(self, rest): game0, game1 = await asyncio.gather( self._agame0.get_restricted_game(rest), self._agame1.get_restricted_game(rest), ) return rsgame.mix(game0, game1, self._prob)
def get_game(self): return rsgame.mix(self._agame0.get_game(), self._agame1.get_game(), self._prob)
def below_regret_thresh(prob, mix_neg): """Event for regret going above threshold""" mix = egame.trim_mixture_support(mix_neg, thresh=0) reg = regret.mixture_regret(rsgame.mix(game0, game1, prob), mix) return reg - regret_thresh
def trace_equilibrium( # pylint: disable=too-many-locals game0, game1, peq, eqm, target, *, regret_thresh=1e-3, max_step=0.1, singular=1e-7, **ivp_args): """Try to trace an equilibrium out to target Takes two games, a fraction that they're mixed (`peq`), and an equilibrium of the mixed game (`eqm`). It then attempts to find the equilibrium at the `target` mixture. It may not reach target, but will return as far as it got. The return value is two parallel arrays for the probabilities with known equilibria and the equilibria. Parameters ---------- game0 : RsGame The first game that's merged. Represents the payoffs when `peq` is 0. game1 : RsGame The second game that's merged. Represents the payoffs when `peq` is 1. peq : float The amount that the two games are merged such that `eqm` is an equilibrium. Must be in [0, 1]. eqm : ndarray An equilibrium when `game0` and `game1` are merged a `peq` fraction. target : float The desired mixture probability to have an equilibrium at. regret_thresh : float, optional The amount of gain from deviating to a strategy outside support can have before it's considered a beneficial deviation and the tracing stops. This should be larger than zero as most equilibria are approximate due to floating point precision. max_step : float, optional The maximum step to take in t when evaluating. singular : float, optional An absolute determinant below this value is considered singular. Occasionally the derivative doesn't exist, and this is one way in which that manifests. This values regulate when ODE solving terminates due to a singular matrix. ivp_args Any remaining keyword arguments are passed to the ivp solver. """ egame = rsgame.empty_copy(game0) eqm = np.asarray(eqm, float) utils.check(egame.is_mixture(eqm), "equilibrium wasn't a valid mixture") utils.check( regret.mixture_regret(rsgame.mix(game0, game1, peq), eqm) <= regret_thresh + 1e-7, "equilibrium didn't have regret below threshold") ivp_args.update(max_step=max_step) # It may be handy to have the derivative of this so that the ode solver can # be more efficient, except that computing the derivative w.r.t. t requires # the hessian of the deviation payoffs, which would be complicated and so # far has no use anywhere else. def ode(prob, mix_neg): """ODE function for solve_ivp""" div = np.zeros(egame.num_strats) mix = egame.trim_mixture_support(mix_neg, thresh=0) supp = mix > 0 rgame = egame.restrict(supp) dev1, jac1 = game0.deviation_payoffs(mix, jacobian=True) dev2, jac2 = game1.deviation_payoffs(mix, jacobian=True) gvals = (dev1 - dev2)[supp] fvecs = ((1 - prob) * jac1 + prob * jac2)[supp][:, supp] gvec = np.concatenate([ np.delete(np.diff(gvals), rgame.role_starts[1:] - 1), np.zeros(egame.num_roles) ]) fmat = np.concatenate([ np.delete(np.diff(fvecs, 1, 0), rgame.role_starts[1:] - 1, 0), np.eye(egame.num_roles).repeat(rgame.num_role_strats, 1) ]) if singular < np.abs(np.linalg.det(fmat)): div[supp] = np.linalg.solve(fmat, gvec) return div def below_regret_thresh(prob, mix_neg): """Event for regret going above threshold""" mix = egame.trim_mixture_support(mix_neg, thresh=0) reg = regret.mixture_regret(rsgame.mix(game0, game1, prob), mix) return reg - regret_thresh below_regret_thresh.terminal = True below_regret_thresh.direction = 1 def singular_jacobian(prob, mix_neg): """Event for when jacobian is singular""" mix = egame.trim_mixture_support(mix_neg, thresh=0) supp = mix > 0 rgame = egame.restrict(supp) _, jac1 = game0.deviation_payoffs(mix, jacobian=True) _, jac2 = game1.deviation_payoffs(mix, jacobian=True) fvecs = ((1 - prob) * jac1 + prob * jac2)[supp][:, supp] fmat = np.concatenate([ np.delete(np.diff(fvecs, 1, 0), rgame.role_starts[1:] - 1, 0), np.eye(egame.num_roles).repeat(rgame.num_role_strats, 1) ]) return np.abs(np.linalg.det(fmat)) - singular singular_jacobian.terminal = True singular_jacobian.direction = -1 events = [below_regret_thresh, singular_jacobian] # This is to scope the index def create_support_loss(ind): """Create support loss for every ind""" def support_loss(_, mix): """Support loss event""" return mix[ind] support_loss.direction = -1 return support_loss for strat in range(egame.num_strats): events.append(create_support_loss(strat)) with np.errstate(divide='ignore'): res = integrate.solve_ivp(ode, [peq, target], eqm, events=events, **ivp_args) return res.t, egame.trim_mixture_support(res.y.T, thresh=0)