Esempio n. 1
0
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
Esempio n. 2
0
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
Esempio n. 3
0
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
Esempio n. 4
0
 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),
     }
Esempio n. 5
0
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)
Esempio n. 6
0
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
Esempio n. 7
0
 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)
Esempio n. 8
0
 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)
Esempio n. 9
0
 def get_game(self):
     return rsgame.mix(self._agame0.get_game(), self._agame1.get_game(), self._prob)
Esempio n. 10
0
 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
Esempio n. 11
0
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)