Skip to content

ANL Reference

This package provides a wrapper around NegMAS functionality to generate and run tournaments a la ANL 2024 competition. You mostly only need to use anl2024_tournament in your code. The other helpers are provided to allow for a finer control over the scenarios used.

Example Negotiators

The package provides few example negotiators. Of special importance is the MiCRO negotiator which provides a full implementation of a recently proposed behavioral strategy. Other negotiators are just wrappers over negotiators provided by NegMAS.

anl.anl2024.negotiators.builtins.micro.MiCRO

Bases: SAONegotiator

A simple implementation of the MiCRO negotiation strategy

Remarks
  • This is a simplified implementation of the MiCRO strategy.
  • It is not guaranteed to exactly match the published work.
  • MiCRO was introduced here: de Jonge, Dave. "An Analysis of the Linear Bilateral ANAC Domains Using the MiCRO Benchmark Strategy." Proceedings of the Thirty-First International Joint Conference on Artificial Intelligence, IJCAI. 2022.
  • Note that MiCRO works optimally if both negotiators can concede all the way to agreement. If one of them has a high reservation value preventing it from doing so, or if the allowed number of steps is small, MiCRO will not reach agreement (even against itself).
Source code in anl/anl2024/negotiators/builtins/micro.py
class MiCRO(SAONegotiator):
    """
    A simple implementation of the MiCRO negotiation strategy

    Remarks:
        - This is a simplified implementation of the MiCRO strategy.
        - It is not guaranteed to exactly match the published work.
        - MiCRO was introduced here:
          de Jonge, Dave. "An Analysis of the Linear Bilateral ANAC Domains Using the MiCRO Benchmark Strategy."
          Proceedings of the Thirty-First International Joint Conference on Artificial Intelligence, IJCAI. 2022.
        - Note that MiCRO works optimally if both negotiators can concede all the way to agreement. If one of them
          has a high reservation value preventing it from doing so, or if the allowed number of steps is small, MiCRO
          will not reach agreement (even against itself).
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # initialize local variables
        self.worst_offer_utility: float = float("inf")
        self.sorter = None
        self._received, self._sent = set(), set()

    def __call__(self, state: SAOState) -> SAOResponse:
        # The main implementation of the MiCRO strategy
        assert self.ufun
        # initialize the sorter
        # (This should better be done in on_preferences_changed() to allow
        # for reuse but this is not needed in ANL)
        if self.sorter is None:
            # Presort the outcome space on utility value
            self.sorter = PresortingInverseUtilityFunction(
                self.ufun, rational_only=True, eps=-1, rel_eps=-1
            )
            # Initialize the sorter. This is an O(n log n) operation where n
            # is the number of outcomes
            self.sorter.init()
        # get the current offer and prepare for rejecting it
        offer = state.current_offer

        # If I received something, save it
        if offer is not None:
            self._received.add(offer)

        # Find out my next offer and the acceptable offer
        will_concede = len(self._sent) <= len(self._received)
        # My next offer is either a conceding outcome if will_concede
        # or sampled randomly from my past offers
        next_offer = (
            self.sample_sent() if not will_concede else self.sorter.next_worse()
        )
        # If I exhausted all my rational offers, do not concede
        if next_offer is None:
            will_concede, next_offer = False, self.sample_sent()
        else:
            next_utility = float(self.ufun(next_offer))
            if next_utility < self.ufun.reserved_value:
                will_concede, next_offer = False, self.sample_sent()
        next_utility = float(self.ufun(next_offer))
        # Find my acceptable outcome, will be None if I did not offer anything yet.
        acceptable_utility = (
            self.worst_offer_utility if not will_concede else next_utility
        )

        # The Acceptance Policy for MiCRO (as suggested in the paper)
        # accept if the offer is not worse than my acceptable offer if I am
        # conceding or the best so far if I am not
        offer_utility = float(self.ufun(offer))
        if (
            offer is not None
            and offer_utility >= acceptable_utility
            and offer_utility >= self.ufun.reserved_value
        ):
            return SAOResponse(ResponseType.ACCEPT_OFFER, offer)
        # If I cannot find any offers, I know that there are NO rational
        # outcomes in this negotiation for me and will end it.
        if next_offer is None:
            return SAOResponse(ResponseType.END_NEGOTIATION, None)
        # Offer my next-offer and record it
        self._sent.add(next_offer)
        self.worst_offer_utility = next_utility
        return SAOResponse(ResponseType.REJECT_OFFER, next_offer)

    def sample_sent(self) -> Outcome | None:
        # Get an outcome from the set I sent so far, or my best if I sent nothing
        if not len(self._sent):
            return None
        return random.choice(list(self._sent))

anl.anl2024.negotiators.builtins.nash_seeker.NashSeeker

Bases: SAONegotiator

Assumes that the opponent has a fixed reserved value and seeks the Nash equilibrium.

Parameters:

Name Type Description Default
opponent_reserved_value float

Assumed reserved value for the opponent

0.25
nash_factor

Fraction (or multiple) of the agent utility at the Nash Point (assuming the opponent_reserved_value) that is acceptable

0.9
Source code in anl/anl2024/negotiators/builtins/nash_seeker.py
class NashSeeker(SAONegotiator):
    """Assumes that the opponent has a fixed reserved value and seeks the Nash equilibrium.

    Args:
        opponent_reserved_value: Assumed reserved value for the opponent
        nash_factor: Fraction (or multiple) of the agent utility at the Nash Point (assuming the `opponent_reserved_value`) that is acceptable

    """

    def __init__(
        self, *args, opponent_reserved_value: float = 0.25, nash_factor=0.9, **kwargs
    ):
        super().__init__(*args, **kwargs)
        self._opponent_r = opponent_reserved_value
        self._outcomes: list[Outcome] = []
        self._min_acceptable = float("inf")
        self._nash_factor = nash_factor
        self._best: Outcome = None  # type: ignore

    def on_preferences_changed(self, changes):
        _ = changes  # silenting a typing warning
        # This callback is called at the start of the negotiation after the ufun is set
        assert self.ufun is not None and self.ufun.outcome_space is not None
        # save my best outcome for later use
        self._best = self.ufun.best()
        # check that I have access to  the opponent ufun
        assert self.opponent_ufun is not None
        # set the reserved value of the opponent
        self.opponent_ufun.reserved_value = self._opponent_r
        # consider my and my parther's ufuns
        ufuns = (self.ufun, self.opponent_ufun)
        # list all outcomes
        outcomes = list(self.ufun.outcome_space.enumerate_or_sample())
        # find the pareto-front and the nash point
        frontier_utils, frontier_indices = pareto_frontier(ufuns, outcomes)
        frontier_outcomes = [outcomes[_] for _ in frontier_indices]
        my_frontier_utils = [_[0] for _ in frontier_utils]
        nash = nash_points(ufuns, frontier_utils)  # type: ignore
        if nash:
            # find my utility at the Nash Bargaining Solution.
            my_nash_utility = nash[0][0][0]
        else:
            my_nash_utility = 0.5 * (float(self.ufun.max()) + self.ufun.reserved_value)
        # Set the acceptable utility limit
        self._min_acceptable = my_nash_utility * self._nash_factor
        # Set the set of outcomes to offer from
        self._outcomes = [
            w
            for u, w in zip(my_frontier_utils, frontier_outcomes)
            if u >= self._min_acceptable
        ]

    def __call__(self, state: SAOState) -> SAOResponse:
        # just assert that I have a ufun and I know the outcome space.
        assert self.ufun is not None and self.ufun.outcome_space is not None
        # read the current offer from the state. None means I am starting the negotiation
        offer = state.current_offer
        # Accept the offer if its utility is higher than my utility at the Nash Bargaining Solution with the assumed opponent ufun
        if offer and float(self.ufun(offer)) >= self._min_acceptable:
            return SAOResponse(ResponseType.ACCEPT_OFFER, offer)
        # If I could not find the Nash Bargaining Solution, just offer my best outcome forever.
        if not self._outcomes:
            return SAOResponse(ResponseType.REJECT_OFFER, self._best)
        # Offer some outcome with high utility relative to the Nash Bargaining Solution
        return SAOResponse(ResponseType.REJECT_OFFER, random.choice(self._outcomes))

anl.anl2024.negotiators.builtins.rv_fitter.RVFitter

Bases: SAONegotiator

A simple negotiator that uses curve fitting to learn the reserved value.

Parameters:

Name Type Description Default
min_unique_utilities int

Number of different offers from the opponent before starting to attempt learning their reserved value.

10
e float

The concession exponent used for the agent's offering strategy

5.0
stochasticity float

The level of stochasticity in the offers.

0.1
enable_logging bool

If given, a log will be stored for the estimates.

False

Remarks:

- Assumes that the opponent is using a time-based offering strategy that offers
  the outcome at utility $u(t) = (u_0 - r) - r \exp(t^e)$ where $u_0$ is the utility of
  the first offer (directly read from the opponent ufun), $e$ is an exponent that controls the
  concession rate and $r$ is the reserved value we want to learn.
- After it receives offers with enough different utilities, it starts finding the optimal values
  for $e$ and $r$.
- When it is time to respond, RVFitter, calculates the set of rational outcomes **for both agents**
  based on its knowledge of the opponent ufun (given) and reserved value (learned). It then applies
  the same concession curve defined above to concede over an ordered list of these outcomes.
- Is this better than using the same concession curve on the outcome space without even trying to learn
  the opponent reserved value? Maybe sometimes but empirical evaluation shows that it is not in general.
- Note that the way we check for availability of enough data for training is based on the uniqueness of
  the utility of offers from the opponent (for the opponent). Given that these are real values, this approach
  is suspect because of rounding errors. If two outcomes have the same utility they may appear to have different
  but very close utilities because or rounding errors (or genuine very small differences). Such differences should
  be ignored.
- Note also that we start assuming that the opponent reserved value is 0.0 which means that we are only restricted
  with our own reserved values when calculating the rational outcomes. This is the best case scenario for us because
  we have MORE negotiation power when the partner has LOWER utility.
Source code in anl/anl2024/negotiators/builtins/rv_fitter.py
class RVFitter(SAONegotiator):
    """A simple negotiator that uses curve fitting to learn the reserved value.

    Args:
        min_unique_utilities: Number of different offers from the opponent before starting to
                              attempt learning their reserved value.
        e: The concession exponent used for the agent's offering strategy
        stochasticity: The level of stochasticity in the offers.
        enable_logging: If given, a log will be stored  for the estimates.

    Remarks:

        - Assumes that the opponent is using a time-based offering strategy that offers
          the outcome at utility $u(t) = (u_0 - r) - r \\exp(t^e)$ where $u_0$ is the utility of
          the first offer (directly read from the opponent ufun), $e$ is an exponent that controls the
          concession rate and $r$ is the reserved value we want to learn.
        - After it receives offers with enough different utilities, it starts finding the optimal values
          for $e$ and $r$.
        - When it is time to respond, RVFitter, calculates the set of rational outcomes **for both agents**
          based on its knowledge of the opponent ufun (given) and reserved value (learned). It then applies
          the same concession curve defined above to concede over an ordered list of these outcomes.
        - Is this better than using the same concession curve on the outcome space without even trying to learn
          the opponent reserved value? Maybe sometimes but empirical evaluation shows that it is not in general.
        - Note that the way we check for availability of enough data for training is based on the uniqueness of
          the utility of offers from the opponent (for the opponent). Given that these are real values, this approach
          is suspect because of rounding errors. If two outcomes have the same utility they may appear to have different
          but very close utilities because or rounding errors (or genuine very small differences). Such differences should
          be ignored.
        - Note also that we start assuming that the opponent reserved value is 0.0 which means that we are only restricted
          with our own reserved values when calculating the rational outcomes. This is the best case scenario for us because
          we have MORE negotiation power when the partner has LOWER utility.
    """

    def __init__(
        self,
        *args,
        min_unique_utilities: int = 10,
        e: float = 5.0,
        stochasticity: float = 0.1,
        enable_logging: bool = False,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.min_unique_utilities = min_unique_utilities
        self.e = e
        self.stochasticity = stochasticity
        # keeps track of times at which the opponent offers
        self.opponent_times: list[float] = []
        # keeps track of opponent utilities of its offers
        self.opponent_utilities: list[float] = []
        # keeps track of the our last estimate of the opponent reserved value
        self._past_oppnent_rv = 0.0
        # keeps track of the rational outcome set given our estimate of the
        # opponent reserved value and our knowledge of ours
        self._rational: list[tuple[float, float, Outcome]] = []
        self._enable_logging = enable_logging

    def __call__(self, state: SAOState) -> SAOResponse:
        assert self.ufun and self.opponent_ufun
        # update the opponent reserved value in self.opponent_ufun
        self.update_reserved_value(state)
        # rune the acceptance strategy and if the offer received is acceptable, accept it
        if self.is_acceptable(state):
            return SAOResponse(ResponseType.ACCEPT_OFFER, state.current_offer)
        # The offering strategy
        # We only update our estimate of the rational list of outcomes if it is not set or
        # there is a change in estimated reserved value
        if (
            not self._rational
            or abs(self.opponent_ufun.reserved_value - self._past_oppnent_rv) > 1e-3
        ):
            # The rational set of outcomes sorted dependingly according to our utility function
            # and the opponent utility function (in that order).
            self._rational = sorted(
                [
                    (my_util, opp_util, _)
                    for _ in self.nmi.outcome_space.enumerate_or_sample(
                        levels=10, max_cardinality=100_000
                    )
                    if (my_util := float(self.ufun(_))) > self.ufun.reserved_value
                    and (opp_util := float(self.opponent_ufun(_)))
                    > self.opponent_ufun.reserved_value
                ],
            )
        # If there are no rational outcomes (i.e. our estimate of the opponent rv is very wrogn),
        # then just revert to offering our top offer
        if not self._rational:
            return SAOResponse(ResponseType.REJECT_OFFER, self.ufun.best())
        # find our aspiration level (value between 0 and 1) the higher the higher utility we require
        asp = aspiration_function(state.relative_time, 1.0, 0.0, self.e)
        # find the index of the rational outcome at the aspiration level (in the rational set of outcomes)
        n_rational = len(self._rational)
        max_rational = n_rational - 1
        min_indx = max(0, min(max_rational, int(asp * max_rational)))
        # find current stochasticity which goes down from the set level to zero linearly
        s = aspiration_function(state.relative_time, self.stochasticity, 0.0, 1.0)
        # find the index of the maximum utility we require based on stochasticity (going down over time)
        max_indx = max(0, min(int(min_indx + s * n_rational), max_rational))
        # offer an outcome in the selected range
        indx = random.randint(min_indx, max_indx) if min_indx != max_indx else min_indx
        outcome = self._rational[indx][-1]
        return SAOResponse(ResponseType.REJECT_OFFER, outcome)

    def is_acceptable(self, state: SAOState) -> bool:
        # The acceptance strategy
        assert self.ufun and self.opponent_ufun
        # get the offer from the mechanism state
        offer = state.current_offer
        # If there is no offer, there is nothing to accept
        if offer is None:
            return False
        # Find the current aspiration level
        asp = aspiration_function(
            state.relative_time, 1.0, self.ufun.reserved_value, self.e
        )
        # accept if the utility of the received offer is higher than
        # the current aspiration
        return float(self.ufun(offer)) >= asp

    def update_reserved_value(self, state: SAOState):
        # Learns the reserved value of the partner
        assert self.opponent_ufun is not None
        # extract the current offer from the state
        offer = state.current_offer
        if offer is None:
            return
        # save to the list of utilities received from the opponent and their times
        self.opponent_utilities.append(float(self.opponent_ufun(offer)))
        self.opponent_times.append(state.relative_time)

        # If we do not have enough data, just assume that the opponent
        # reserved value is zero
        n_unique = len(set(self.opponent_utilities))
        if n_unique < self.min_unique_utilities:
            self._past_oppnent_rv = 0.0
            self.opponent_ufun.reserved_value = 0.0
            return
        # Use curve fitting to estimate the opponent reserved value
        # We assume the following:
        # - The opponent is using a concession strategy with an exponent between 0.2, 5.0
        # - The opponent never offers outcomes lower than their reserved value which means
        #   that their rv must be no higher than the worst outcome they offered for themselves.
        bounds = ((0.2, 0.0), (5.0, min(self.opponent_utilities)))
        err = ""
        try:
            optimal_vals, _ = curve_fit(
                lambda x, e, rv: aspiration_function(
                    x, self.opponent_utilities[0], rv, e
                ),
                self.opponent_times,
                self.opponent_utilities,
                bounds=bounds,
            )
            self._past_oppnent_rv = self.opponent_ufun.reserved_value
            self.opponent_ufun.reserved_value = optimal_vals[1]
        except Exception as e:
            err, optimal_vals = f"{str(e)}", [None, None]

        # log my estimate
        if self._enable_logging:
            self.nmi.log_info(
                self.id,
                dict(
                    estimated_rv=self.opponent_ufun.reserved_value,
                    n_unique=n_unique,
                    opponent_utility=self.opponent_utilities[-1],
                    estimated_exponent=optimal_vals[0],
                    estimated_max=self.opponent_utilities[0],
                    error=err,
                ),
            )

anl.anl2024.negotiators.builtins.wrappers.Boulware

Bases: BoulwareTBNegotiator

Time-based boulware negotiation strategy

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class Boulware(BoulwareTBNegotiator):
    """
    Time-based boulware negotiation strategy
    """

anl.anl2024.negotiators.builtins.wrappers.Linear

Bases: LinearTBNegotiator

Time-based linear negotiation strategy

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class Linear(LinearTBNegotiator):
    """
    Time-based linear negotiation strategy
    """

    ...

anl.anl2024.negotiators.builtins.wrappers.Conceder

Bases: ConcederTBNegotiator

Time-based conceder negotiation strategy

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class Conceder(ConcederTBNegotiator):
    """
    Time-based conceder negotiation strategy
    """

anl.anl2024.negotiators.builtins.wrappers.StochasticBoulware

Bases: AspirationNegotiator

Time-based boulware negotiation strategy (offers above the limit instead of at it)

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class StochasticBoulware(AspirationNegotiator):
    """
    Time-based boulware negotiation strategy (offers above the limit instead of at it)
    """

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            max_aspiration=1.0,
            aspiration_type="boulware",
            tolerance=0.00001,
            stochastic=True,
            presort=True,
            **kwargs
        )

anl.anl2024.negotiators.builtins.wrappers.StochasticLinear

Bases: AspirationNegotiator

Time-based linear negotiation strategy (offers above the limit instead of at it)

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class StochasticLinear(AspirationNegotiator):
    """
    Time-based linear negotiation strategy (offers above the limit instead of at it)
    """

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            max_aspiration=1.0,
            aspiration_type="linear",
            tolerance=0.00001,
            stochastic=True,
            presort=True,
            **kwargs
        )

anl.anl2024.negotiators.builtins.wrappers.StochasticConceder

Bases: AspirationNegotiator

Time-based conceder negotiation strategy (offers above the limit instead of at it)

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class StochasticConceder(AspirationNegotiator):
    """
    Time-based conceder negotiation strategy (offers above the limit instead of at it)
    """

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            max_aspiration=1.0,
            aspiration_type="conceder",
            tolerance=0.00001,
            stochastic=True,
            presort=True,
            **kwargs
        )

anl.anl2024.negotiators.builtins.wrappers.NaiveTitForTat

Bases: NaiveTitForTatNegotiator

A simple behavioral strategy that assumes a zero-sum game

Source code in anl/anl2024/negotiators/builtins/wrappers.py
class NaiveTitForTat(NaiveTitForTatNegotiator):
    """
    A simple behavioral strategy that assumes a zero-sum game
    """

Tournaments

anl.anl2024.anl2024_tournament

Runs an ANL 2024 tournament

Parameters:

Name Type Description Default
scenarios tuple[Scenario, ...] | list[Scenario]

A list of predefined scenarios to use for the tournament

tuple()
n_scenarios int

Number of negotiation scenarios to generate specifically for this tournament

DEFAULT2024SETTINGS['n_scenarios']
n_outcomes int | tuple[int, int] | list[int]

Number of outcomes (or a min/max tuple of n. outcomes) for each scenario

DEFAULT2024SETTINGS['n_outcomes']
competitors tuple[type[Negotiator] | str, ...] | list[type[Negotiator] | str]

list of competitor agents

DEFAULT_AN2024_COMPETITORS
competitor_params Sequence[dict | None] | None

If given, parameters to construct each competitor

None
rotate_ufuns bool

If given, each scenario will be tried with both orders of the ufuns.

DEFAULT2024SETTINGS['rotate_ufuns']
n_repetitions int

Number of times to repeat each negotiation

DEFAULT2024SETTINGS['n_repetitions']
n_steps int | tuple[int, int] | None

Number of steps/rounds allowed for the each negotiation (None for no-limit and a 2-valued tuple for sampling from a range)

DEFAULT2024SETTINGS['n_steps']
time_limit float | tuple[float, float] | None

Number of seconds allowed for the each negotiation (None for no-limit and a 2-valued tuple for sampling from a range)

DEFAULT2024SETTINGS['time_limit']
pend float | tuple[float, float]

Probability of ending the negotiation every step/round (None for no-limit and a 2-valued tuple for sampling from a range)

DEFAULT2024SETTINGS['pend']
pend_per_second float | tuple[float, float]

Probability of ending the negotiation every second (None for no-limit and a 2-valued tuple for sampling from a range)

DEFAULT2024SETTINGS['pend_per_second']
step_time_limit float | tuple[float, float] | None

Time limit for every negotiation step (None for no-limit and a 2-valued tuple for sampling from a range)

DEFAULT2024SETTINGS['step_time_limit']
negotiator_time_limit float | tuple[float, float] | None

Time limit for all actions of every negotiator (None for no-limit and a 2-valued tuple for sampling from a range)

DEFAULT2024SETTINGS['negotiator_time_limit']
name str | None

Name of the tournament

None
nologs bool

If given, no logs will be saved

False
njobs int

Number of parallel jobs to use. -1 for serial and 0 for all cores

0
plot_fraction float

Fraction of negotiations to plot. Only used if not nologs

0.2
verbosity int

Verbosity level. The higher the more verbose

1
self_play bool

Allow negotiators to run against themselves.

DEFAULT2024SETTINGS['self_play']
randomize_runs bool

Randomize the order of negotiations

DEFAULT2024SETTINGS['randomize_runs']
save_every int

Save logs every this number of negotiations

0
save_stats bool

Save statistics for scenarios

True
known_partner bool

Allow negotiators to know the type of their partner (through their ID)

DEFAULT2024SETTINGS['known_partner']
final_score tuple[str, str]

The metric and statistic used to calculate the score. Metrics are: advantage, utility, welfare, partner_welfare and Stats are: median, mean, std, min, max

DEFAULT2024SETTINGS['final_score']
base_path Path | None

Folder in which to generate the logs folder for this tournament. Default is ~/negmas/anl2024/tournaments

None
scenario_generator str | ScenarioGenerator

An alternative method for generating bilateral negotiation scenarios. Must receive the number of scenarios and number of outcomes.

DEFAULT2024SETTINGS['scenario_generator']
generator_params dict[str, Any] | None

Parameters passed to the scenario generator

DEFAULT2024SETTINGS['generator_params']
plot_params dict[str, Any] | None

If given, overrides plotting parameters. See nemgas.sao.SAOMechanism.plot() for all parameters

None

Returns:

Type Description
SimpleTournamentResults

Tournament results as a SimpleTournamentResults object.

Source code in anl/anl2024/runner.py
def anl2024_tournament(
    scenarios: tuple[Scenario, ...] | list[Scenario] = tuple(),
    n_scenarios: int = DEFAULT2024SETTINGS["n_scenarios"],  # type: ignore
    n_outcomes: int | tuple[int, int] | list[int] = DEFAULT2024SETTINGS["n_outcomes"],  # type: ignore
    competitors: tuple[type[Negotiator] | str, ...]
    | list[type[Negotiator] | str] = DEFAULT_AN2024_COMPETITORS,
    rotate_ufuns: bool = DEFAULT2024SETTINGS["rotate_ufuns"],  # type: ignore
    n_repetitions: int = DEFAULT2024SETTINGS["n_repetitions"],  # type: ignore
    n_steps: int | tuple[int, int] | None = DEFAULT2024SETTINGS["n_steps"],  # type: ignore
    time_limit: float | tuple[float, float] | None = DEFAULT2024SETTINGS["time_limit"],  # type: ignore
    pend: float | tuple[float, float] = DEFAULT2024SETTINGS["pend"],  # type: ignore
    pend_per_second: float | tuple[float, float] = DEFAULT2024SETTINGS[
        "pend_per_second"
    ],  # type: ignore
    step_time_limit: float | tuple[float, float] | None = DEFAULT2024SETTINGS[
        "step_time_limit"
    ],  # type: ignore
    negotiator_time_limit: float | tuple[float, float] | None = DEFAULT2024SETTINGS[
        "negotiator_time_limit"
    ],  # type: ignore
    self_play: bool = DEFAULT2024SETTINGS["self_play"],  # type: ignore
    randomize_runs: bool = DEFAULT2024SETTINGS["randomize_runs"],  # type: ignore
    known_partner: bool = DEFAULT2024SETTINGS["known_partner"],  # type: ignore
    final_score: tuple[str, str] = DEFAULT2024SETTINGS["final_score"],  # type: ignore
    scenario_generator: str | ScenarioGenerator = DEFAULT2024SETTINGS[
        "scenario_generator"
    ],  # type: ignore
    generator_params: dict[str, Any] | None = DEFAULT2024SETTINGS["generator_params"],  # type: ignore
    competitor_params: Sequence[dict | None] | None = None,
    name: str | None = None,
    nologs: bool = False,
    njobs: int = 0,
    plot_fraction: float = 0.2,
    verbosity: int = 1,
    save_every: int = 0,
    save_stats: bool = True,
    base_path: Path | None = None,
    plot_params: dict[str, Any] | None = None,
    raise_exceptions: bool = True,
) -> SimpleTournamentResults:
    """Runs an ANL 2024 tournament

    Args:
        scenarios: A list of predefined scenarios to use for the tournament
        n_scenarios: Number of negotiation scenarios to generate specifically for this tournament
        n_outcomes: Number of outcomes (or a min/max tuple of n. outcomes) for each scenario
        competitors: list of competitor agents
        competitor_params: If given, parameters to construct each competitor
        rotate_ufuns: If given, each scenario will be tried with both orders of the ufuns.
        n_repetitions: Number of times to repeat each negotiation
        n_steps: Number of steps/rounds allowed for the each negotiation (None for no-limit and a 2-valued tuple for sampling from a range)
        time_limit: Number of seconds allowed for the each negotiation (None for no-limit and a 2-valued tuple for sampling from a range)
        pend: Probability of ending the negotiation every step/round (None for no-limit and a 2-valued tuple for sampling from a range)
        pend_per_second: Probability of ending the negotiation every second (None for no-limit and a 2-valued tuple for sampling from a range)
        step_time_limit: Time limit for every negotiation step (None for no-limit and a 2-valued tuple for sampling from a range)
        negotiator_time_limit: Time limit for all actions of every negotiator (None for no-limit and a 2-valued tuple for sampling from a range)
        name: Name of the tournament
        nologs: If given, no logs will be saved
        njobs: Number of parallel jobs to use. -1 for serial and 0 for all cores
        plot_fraction: Fraction of negotiations to plot. Only used if not nologs
        verbosity: Verbosity level. The higher the more verbose
        self_play: Allow negotiators to run against themselves.
        randomize_runs: Randomize the order of negotiations
        save_every: Save logs every this number of negotiations
        save_stats: Save statistics for scenarios
        known_partner: Allow negotiators to know the type of their partner (through their ID)
        final_score: The metric and statistic used to calculate the score. Metrics are: advantage, utility, welfare, partner_welfare and Stats are: median, mean, std, min, max
        base_path: Folder in which to generate the logs folder for this tournament. Default is ~/negmas/anl2024/tournaments
        scenario_generator: An alternative method for generating bilateral negotiation scenarios. Must receive the number of scenarios and number of outcomes.
        generator_params: Parameters passed to the scenario generator
        plot_params: If given, overrides plotting parameters. See `nemgas.sao.SAOMechanism.plot()` for all parameters

    Returns:
        Tournament results as a `SimpleTournamentResults` object.
    """
    if generator_params is None:
        generator_params = dict()
    if isinstance(scenario_generator, str):
        scenario_generator = GENMAP[scenario_generator]
    all_outcomes = not scenario_generator == zerosum_pie_scenarios
    if nologs:
        path = None
    elif base_path is not None:
        path = Path(base_path) / (name if name else unique_name("anl"))
    else:
        path = DEFAULT_TOURNAMENT_PATH / (name if name else unique_name("anl"))
    params = dict(
        ylimits=(0, 1),
        mark_offers_view=True,
        mark_pareto_points=all_outcomes,
        mark_all_outcomes=all_outcomes,
        mark_nash_points=True,
        mark_kalai_points=all_outcomes,
        mark_max_welfare_points=all_outcomes,
        show_agreement=True,
        show_pareto_distance=False,
        show_nash_distance=True,
        show_kalai_distance=False,
        show_max_welfare_distance=False,
        show_max_relative_welfare_distance=False,
        show_end_reason=True,
        show_annotations=not all_outcomes,
        show_reserved=True,
        show_total_time=True,
        show_relative_time=True,
        show_n_steps=True,
    )
    if plot_params:
        params = params.update(plot_params)
    scenarios = list(scenarios) + list(
        scenario_generator(n_scenarios, n_outcomes, **generator_params)
    )
    private_infos = [
        tuple(
            dict(
                opponent_ufun=U(
                    values=_.values,  # type: ignore
                    weights=_.weights,  # type: ignore
                    bias=_._bias,  # type: ignore
                    reserved_value=0,
                    outcome_space=_.outcome_space,
                )
            )  # type: ignore
            for _ in s.ufuns[::-1]
        )
        for s in scenarios
    ]
    return cartesian_tournament(
        competitors=tuple(competitors),
        scenarios=scenarios,
        competitor_params=competitor_params,
        private_infos=private_infos,  # type: ignore
        rotate_ufuns=rotate_ufuns,
        n_repetitions=n_repetitions,
        path=path,
        njobs=njobs,
        mechanism_type=SAOMechanism,
        n_steps=n_steps,
        time_limit=time_limit,
        pend=pend,
        pend_per_second=pend_per_second,
        step_time_limit=step_time_limit,
        negotiator_time_limit=negotiator_time_limit,
        mechanism_params=None,
        plot_fraction=plot_fraction,
        verbosity=verbosity,
        self_play=self_play,
        randomize_runs=randomize_runs,
        save_every=save_every,
        save_stats=save_stats,
        final_score=final_score,
        id_reveals_type=known_partner,
        name_reveals_type=True,
        plot_params=params,
        raise_exceptions=raise_exceptions,
    )

anl.anl2024.DEFAULT_AN2024_COMPETITORS = (RVFitter, NashSeeker, MiCRO, Boulware, Conceder, Linear) module-attribute

Default set of negotiators (agents) used as competitors

anl.anl2024.DEFAULT_TOURNAMENT_PATH = Path.home() / 'negmas' / 'anl2024' / 'tournaments' module-attribute

Default location to store tournament logs

anl.anl2024.DEFAULT2024SETTINGS = dict(n_ufuns=2, n_scenarios=50, n_outcomes=(900, 1100), n_steps=(10, 10000), n_repetitions=5, reserved_ranges=((0.0, 1.0), (0.0, 1.0)), competitors=DEFAULT_AN2024_COMPETITORS, rotate_ufuns=False, time_limit=3 * 60, pend=0, pend_per_second=0, step_time_limit=None, negotiator_time_limit=None, self_play=True, randomize_runs=True, known_partner=False, final_score=('advantage', 'mean'), scenario_generator='mix', outcomes_log_uniform=True, generator_params=dict(reserved_ranges=((0.0, 1.0), (0.0, 1.0)), log_uniform=False, zerosum_fraction=0.05, monotonic_fraction=0.25, curve_fraction=0.25, pies_fraction=0.2, pareto_first=False, n_pareto=(0.005, 0.25))) module-attribute

Default settings for ANL 2024

Helpers (Scenario Generation)

anl.anl2024.ScenarioGenerator = Callable[[int, int | tuple[int, int] | list[int]], list[Scenario]] module-attribute

Type of callable that can be used for generating scenarios. It must receive the number of scenarios and number of outcomes (as int, tuple or list) and return a list of Scenario s

anl.anl2024.mixed_scenarios

Generates a mix of zero-sum, monotonic and general scenarios

Parameters:

Name Type Description Default
n_scenarios int

Number of scenarios to genearate

DEFAULT2024SETTINGS['n_scenarios']
n_outcomes int | tuple[int, int] | list[int]

Number of outcomes (or a list of range thereof).

DEFAULT2024SETTINGS['n_outcomes']
reserved_ranges ReservedRanges

the range allowed for reserved values for each ufun. Note that the upper limit will be overridden to guarantee the existence of at least one rational outcome

DEFAULT2024SETTINGS['reserved_ranges']
log_uniform bool

Use log-uniform instead of uniform sampling if n_outcomes is a tuple giving a range.

DEFAULT2024SETTINGS['outcomes_log_uniform']
zerosum_fraction float

Fraction of zero-sum scenarios. These are original DivideThePie scenarios

DEFAULT2024SETTINGS['generator_params']['zerosum_fraction']
monotonic_fraction float

Fraction of scenarios where each ufun is a monotonic function of the received pie.

DEFAULT2024SETTINGS['generator_params']['monotonic_fraction']
curve_fraction float

Fraction of general and monotonic scenarios that use a curve for Pareto generation instead of a piecewise linear Pareto frontier.

DEFAULT2024SETTINGS['generator_params']['curve_fraction']
pies_fraction float

Fraction of divide-the-pies multi-issue scenarios

DEFAULT2024SETTINGS['generator_params']['pies_fraction']
pareto_first bool

If given, the Pareto frontier will always be in the first set of outcomes

DEFAULT2024SETTINGS['generator_params']['pareto_first']
n_ufuns int

Number of ufuns to generate per scenario

DEFAULT2024SETTINGS['n_ufuns']
n_pareto int | float | tuple[float | int, float | int] | list[int | float]

Number of outcomes on the Pareto frontier in general scenarios. Can be specified as a number, a tuple of a min and max to sample within, a list of possibilities. Each value can either be an integer > 1 or a fraction of the number of outcomes in the scenario.

DEFAULT2024SETTINGS['generator_params']['n_pareto']
pareto_log_uniform bool

Use log-uniform instead of uniform sampling if n_pareto is a tuple

False
n_trials

Number of times to retry generating each scenario if failures occures

10

Returns:

Type Description
list[Scenario]

A list Scenario s

Source code in anl/anl2024/runner.py
def mixed_scenarios(
    n_scenarios: int = DEFAULT2024SETTINGS["n_scenarios"],  # type: ignore
    n_outcomes: int | tuple[int, int] | list[int] = DEFAULT2024SETTINGS["n_outcomes"],  # type: ignore
    *,
    reserved_ranges: ReservedRanges = DEFAULT2024SETTINGS["reserved_ranges"],  # type: ignore
    log_uniform: bool = DEFAULT2024SETTINGS["outcomes_log_uniform"],  # type: ignore
    zerosum_fraction: float = DEFAULT2024SETTINGS["generator_params"][  # type: ignore
        "zerosum_fraction"
    ],
    monotonic_fraction: float = DEFAULT2024SETTINGS["generator_params"][  # type: ignore
        "monotonic_fraction"
    ],
    curve_fraction: float = DEFAULT2024SETTINGS["generator_params"]["curve_fraction"],  # type: ignore
    pies_fraction: float = DEFAULT2024SETTINGS["generator_params"]["pies_fraction"],  # type: ignore
    pareto_first: bool = DEFAULT2024SETTINGS["generator_params"]["pareto_first"],  # type: ignore
    n_ufuns: int = DEFAULT2024SETTINGS["n_ufuns"],  # type: ignore
    n_pareto: int
    | float
    | tuple[float | int, float | int]
    | list[int | float] = DEFAULT2024SETTINGS["generator_params"]["n_pareto"],  # type: ignore
    pareto_log_uniform: bool = False,
    n_trials=10,
) -> list[Scenario]:
    """Generates a mix of zero-sum, monotonic and general scenarios

    Args:
        n_scenarios: Number of scenarios to genearate
        n_outcomes: Number of outcomes (or a list of range thereof).
        reserved_ranges: the range allowed for reserved values for each ufun.
                         Note that the upper limit will be overridden to guarantee
                         the existence of at least one rational outcome
        log_uniform: Use log-uniform instead of uniform sampling if n_outcomes is a tuple giving a range.
        zerosum_fraction: Fraction of zero-sum scenarios. These are original DivideThePie scenarios
        monotonic_fraction: Fraction of scenarios where each ufun is a monotonic function of the received pie.
        curve_fraction: Fraction of general and monotonic scenarios that use a curve for Pareto generation instead of
                        a piecewise linear Pareto frontier.
        pies_fraction: Fraction of divide-the-pies multi-issue scenarios
        pareto_first: If given, the Pareto frontier will always be in the first set of outcomes
        n_ufuns: Number of ufuns to generate per scenario
        n_pareto: Number of outcomes on the Pareto frontier in general scenarios.
                Can be specified as a number, a tuple of a min and max to sample within, a list of possibilities.
                Each value can either be an integer > 1 or a fraction of the number of outcomes in the scenario.
        pareto_log_uniform: Use log-uniform instead of uniform sampling if n_pareto is a tuple
        n_trials: Number of times to retry generating each scenario if failures occures

    Returns:
        A list `Scenario` s
    """
    assert zerosum_fraction + monotonic_fraction <= 1.0
    nongeneral_fraction = zerosum_fraction + monotonic_fraction
    ufun_sets = []
    for i in range(n_scenarios):
        r = random.random()
        n = intin(n_outcomes, log_uniform)
        name = "S"
        if r < nongeneral_fraction:
            n_pareto_selected = n
            name = "DivideThePieGen"
        else:
            if isinstance(n_pareto, Iterable):
                n_pareto = type(n_pareto)(
                    int(_ * n + 0.5) if _ < 1 else int(_)
                    for _ in n_pareto  # type: ignore
                )
            else:
                n_pareto = int(0.5 + n_pareto * n) if n_pareto < 1 else int(n_pareto)
            n_pareto_selected = intin(n_pareto, log_uniform=pareto_log_uniform)  # type: ignore
        ufuns, vals = None, None
        for _ in range(n_trials):
            try:
                if r < zerosum_fraction:
                    name = "DivideThePie"
                    vals = generate_utility_values(
                        n_pareto=n_pareto_selected,
                        n_outcomes=n,
                        n_ufuns=n_ufuns,
                        pareto_first=pareto_first,
                        pareto_generator="zero_sum",
                    )
                elif r < zerosum_fraction + pies_fraction:
                    name = "DivideThePies"
                    ufuns = generate_multi_issue_ufuns(
                        n_issues=3,
                        n_values=(9, 11),
                        pareto_generators=tuple(GENERATOR_MAP.keys()),
                        ufun_names=("First0", "Second1"),
                        os_name=f"{name}{i}",
                    )
                else:
                    if n_pareto_selected < 2:
                        n_pareto_selected = 2
                    vals = generate_utility_values(
                        n_pareto=n_pareto_selected,
                        n_outcomes=n,
                        n_ufuns=n_ufuns,
                        pareto_first=pareto_first,
                        pareto_generator="curve"
                        if random.random() < curve_fraction
                        else "piecewise_linear",
                    )
                break
            except:
                continue
        else:
            continue

        if ufuns is None:
            issues = (make_issue([f"{i}_{n-1 - i}" for i in range(n)], "portions"),)
            ufuns = tuple(
                U(
                    values=(
                        TableFun(
                            {_: float(vals[i][k]) for i, _ in enumerate(issues[0].all)}  # type: ignore
                        ),
                    ),
                    name=f"{uname}{i}",
                    # reserved_value=(r[0] + random.random() * (r[1] - r[0] - 1e-8)),
                    outcome_space=make_os(issues, name=f"{name}{i}"),
                )
                for k, uname in enumerate(("First", "Second"))
                # for k, (uname, r) in enumerate(zip(("First", "Second"), reserved_ranges))
            )
        sample_reserved_values(ufuns, reserved_ranges=reserved_ranges)
        ufun_sets.append(ufuns)

    return [
        Scenario(
            outcome_space=ufuns[0].outcome_space,  # type: ignore We are sure this is not None
            ufuns=ufuns,
        )
        for ufuns in ufun_sets
    ]

anl.anl2024.pie_scenarios

Creates single-issue scenarios with arbitrary/monotonically increasing utility functions

Parameters:

Name Type Description Default
n_scenarios int

Number of scenarios to create

20
n_outcomes int | tuple[int, int] | list[int]

Number of outcomes per scenario. If a tuple it will be interpreted as a min/max range to sample n. outcomes from. If a list, samples from this list will be used (with replacement).

100
reserved_ranges ReservedRanges

Ranges of reserved values for first and second negotiators

((0.0, 0.999999), (0.0, 0.999999))
log_uniform bool

If given, the distribution used will be uniform on the logarithm of n. outcomes (only used when n_outcomes is a 2-valued tuple).

True
monotonic

If true all ufuns are monotonically increasing in the portion of the pie

False
Remarks
  • When n_outcomes is a tuple, the number of outcomes for each scenario will be sampled independently.
Source code in anl/anl2024/runner.py
def pie_scenarios(
    n_scenarios: int = 20,
    n_outcomes: int | tuple[int, int] | list[int] = 100,
    *,
    reserved_ranges: ReservedRanges = ((0.0, 0.999999), (0.0, 0.999999)),
    log_uniform: bool = True,
    monotonic=False,
) -> list[Scenario]:
    """Creates single-issue scenarios with arbitrary/monotonically increasing utility functions

    Args:
        n_scenarios: Number of scenarios to create
        n_outcomes: Number of outcomes per scenario. If a tuple it will be interpreted as a min/max range to sample n. outcomes from.
                    If a list, samples from this list will be used (with replacement).
        reserved_ranges: Ranges of reserved values for first and second negotiators
        log_uniform: If given, the distribution used will be uniform on the logarithm of n. outcomes (only used when n_outcomes is a 2-valued tuple).
        monotonic: If true all ufuns are monotonically increasing in the portion of the pie

    Remarks:
        - When n_outcomes is a tuple, the number of outcomes for each scenario will be sampled independently.
    """
    ufun_sets = []
    base_name = "DivideTyePie" if monotonic else "S"

    def normalize(x):
        mn, mx = x.min(), x.max()
        return ((x - mn) / (mx - mn)).tolist()

    def make_monotonic(x, i):
        x = np.sort(np.asarray(x), axis=None)

        if i:
            x = x[::-1]
        r = random.random()
        if r < 0.33:
            x = np.exp(x)
        elif r < 0.67:
            x = np.log(x)
        else:
            pass
        return normalize(x)

    max_jitter_level = 0.8
    for i in range(n_scenarios):
        n = intin(n_outcomes, log_uniform)
        issues = (
            make_issue(
                [f"{i}_{n-1 - i}" for i in range(n)],
                "portions" if not monotonic else "i1",
            ),
        )
        # funs = [
        #     dict(
        #         zip(
        #             issues[0].all,
        #             # adjust(np.asarray([random.random() for _ in range(n)])),
        #             generate(n, i),
        #         )
        #     )
        #     for i in range(2)
        # ]
        os = make_os(issues, name=f"{base_name}{i}")
        outcomes = list(os.enumerate_or_sample())
        ufuns = U.generate_bilateral(
            outcomes,
            conflict_level=0.5 + 0.5 * random.random(),
            conflict_delta=random.random(),
        )
        jitter_level = random.random() * max_jitter_level
        funs = [
            np.asarray([float(u(_)) for _ in outcomes])
            + np.random.random() * jitter_level
            for u in ufuns
        ]

        if monotonic:
            funs = [make_monotonic(x, i) for i, x in enumerate(funs)]
        else:
            funs = [normalize(x) for x in funs]
        ufuns = tuple(
            U(
                values=(TableFun(dict(zip(issues[0].all, vals))),),
                name=f"{uname}{i}",
                outcome_space=os,
                # reserved_value=(r[0] + random.random() * (r[1] - r[0] - 1e-8)),
            )
            for (uname, vals) in zip(("First", "Second"), funs)
            # for (uname, r, vals) in zip(("First", "Second"), reserved_ranges, funs)
        )
        sample_reserved_values(ufuns, reserved_ranges=reserved_ranges)
        ufun_sets.append(ufuns)

    return [
        Scenario(
            outcome_space=ufuns[0].outcome_space,  # type: ignore We are sure this is not None
            ufuns=ufuns,
        )
        for ufuns in ufun_sets
    ]

anl.anl2024.arbitrary_pie_scenarios

Source code in anl/anl2024/runner.py
def arbitrary_pie_scenarios(
    n_scenarios: int = 20,
    n_outcomes: int | tuple[int, int] | list[int] = 100,
    *,
    reserved_ranges: ReservedRanges = ((0.0, 0.999999), (0.0, 0.999999)),
    log_uniform: bool = True,
) -> list[Scenario]:
    return pie_scenarios(
        n_scenarios,
        n_outcomes,
        reserved_ranges=reserved_ranges,
        log_uniform=log_uniform,
        monotonic=False,
    )

anl.anl2024.monotonic_pie_scenarios

Source code in anl/anl2024/runner.py
def monotonic_pie_scenarios(
    n_scenarios: int = 20,
    n_outcomes: int | tuple[int, int] | list[int] = 100,
    *,
    reserved_ranges: ReservedRanges = ((0.0, 0.999999), (0.0, 0.999999)),
    log_uniform: bool = True,
) -> list[Scenario]:
    return pie_scenarios(
        n_scenarios,
        n_outcomes,
        reserved_ranges=reserved_ranges,
        log_uniform=log_uniform,
        monotonic=True,
    )

anl.anl2024.zerosum_pie_scenarios

Creates scenarios all of the DivideThePie variety with proportions giving utility

Parameters:

Name Type Description Default
n_scenarios int

Number of scenarios to create

20
n_outcomes int | tuple[int, int] | list[int]

Number of outcomes per scenario (if a tuple it will be interpreted as a min/max range to sample n. outcomes from).

100
reserved_ranges ReservedRanges

Ranges of reserved values for first and second negotiators

((0.0, 0.499999), (0.0, 0.499999))
log_uniform bool

If given, the distribution used will be uniform on the logarithm of n. outcomes (only used when n_outcomes is a 2-valued tuple).

True
Remarks
  • When n_outcomes is a tuple, the number of outcomes for each outcome will be sampled independently
Source code in anl/anl2024/runner.py
def zerosum_pie_scenarios(
    n_scenarios: int = 20,
    n_outcomes: int | tuple[int, int] | list[int] = 100,
    *,
    reserved_ranges: ReservedRanges = ((0.0, 0.499999), (0.0, 0.499999)),
    log_uniform: bool = True,
) -> list[Scenario]:
    """Creates scenarios all of the DivideThePie variety with proportions giving utility

    Args:
        n_scenarios: Number of scenarios to create
        n_outcomes: Number of outcomes per scenario (if a tuple it will be interpreted as a min/max range to sample n. outcomes from).
        reserved_ranges: Ranges of reserved values for first and second negotiators
        log_uniform: If given, the distribution used will be uniform on the logarithm of n. outcomes (only used when n_outcomes is a 2-valued tuple).

    Remarks:
        - When n_outcomes is a tuple, the number of outcomes for each outcome will be sampled independently
    """
    ufun_sets = []
    for i in range(n_scenarios):
        n = intin(n_outcomes, log_uniform)
        issues = (make_issue([f"{i}_{n-1 - i}" for i in range(n)], "portions"),)
        ufuns = tuple(
            U(
                values=(
                    TableFun(
                        {
                            _: float(int(str(_).split("_")[k]) / (n - 1))
                            for _ in issues[0].all
                        }
                    ),
                ),
                name=f"{uname}{i}",
                # reserved_value=(r[0] + random.random() * (r[1] - r[0] - 1e-8)),
                outcome_space=make_os(issues, name=f"DivideTyePie{i}"),
            )
            for k, uname in enumerate(("First", "Second"))
            # for k, (uname, r) in enumerate(zip(("First", "Second"), reserved_ranges))
        )
        sample_reserved_values(
            ufuns,
            pareto=tuple(
                tuple(u(_) for u in ufuns)
                for _ in make_os(issues).enumerate_or_sample()
            ),
            reserved_ranges=reserved_ranges,
        )
        ufun_sets.append(ufuns)

    return [
        Scenario(
            outcome_space=ufuns[0].outcome_space,  # type: ignore We are sure this is not None
            ufuns=ufuns,
        )
        for ufuns in ufun_sets
    ]