Source code for scml.oneshot.agents.rand

import random
from itertools import chain, combinations

from negmas import ResponseType
from negmas.outcomes import Outcome
from negmas.sao import SAOResponse, SAOState

from scml.common import distribute
from scml.oneshot.agent import (
    OneShotAgent,
    OneShotSingleAgreementAgent,
    OneShotSyncAgent,
)
from scml.oneshot.common import QUANTITY, TIME, UNIT_PRICE

__all__ = [
    "RandomOneShotAgent",
    "RandDistOneShotAgent",
    "EqualDistOneShotAgent",
    "SyncRandomOneShotAgent",
    "SingleAgreementRandomAgent",
]

PROB_ACCEPTANCE = 0.1
PROB_END = 0.005


def powerset(iterable):
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))


[docs] class RandomOneShotAgent(OneShotAgent): """An agent that randomly leaves the negotiation, accepts or counters with random outcomes""" def __init__( self, *args, p_accept=PROB_ACCEPTANCE, p_end=PROB_END, **kwargs, ): self.p_accept, self.p_end = p_accept + p_end, p_end super().__init__(*args, **kwargs)
[docs] def _random_offer(self, negotiator_id: str): nmi = self.get_nmi(negotiator_id) if not nmi: return None # will end the negotiation return nmi.random_outcome()
[docs] def propose(self, negotiator_id, state) -> Outcome | None: return self._random_offer(negotiator_id)
[docs] def respond(self, negotiator_id, state, source=None) -> ResponseType: r = random.random() if r < self.p_end: return ResponseType.END_NEGOTIATION if r < self.p_accept: return ResponseType.ACCEPT_OFFER return ResponseType.REJECT_OFFER
class NiceAgent(RandomOneShotAgent): """An agent that offers randomly and accepts anything""" def __init__(self, *args, **kwargs): super().__init__(*args, p_accept=0, p_end=0, **kwargs) def respond(self, negotiator_id, state, source=None) -> ResponseType: return ResponseType.ACCEPT_OFFER
[docs] class SyncRandomOneShotAgent(OneShotSyncAgent): """ An agent that distributes its needs over its partners randomly. Args: equal: If given, it tries to equally distribute its needs over as many of its suppliers/consumers as possible overordering_max: Maximum fraction of needs to over-order. For example, it the agent needs 5 items and this is 0.2, it will order 6 in the first negotiation step. overordering_min: Minimum fraction of needs to over-order. Used in the last negotiation step. overordering_exp: Controls how fast does the over-ordering quantity go from max to min. concession_exp: Controls how fast does the agent concedes on matching its needs exactly. mismatch_max: Maximum mismtach in quantity allowed between needs and accepted offers. If a fraction, it is will be this fraction of the production capacity (n_lines). """ def __init__( self, *args, equal: bool = False, overordering_max: float = 0.2, overordering_min: float = 0.0, overordering_exp: float = 0.4, mismatch_exp: float = 4.0, mismatch_max: float = 0.3, **kwargs, ): self.equal_distribution = equal self.overordering_max = overordering_max self.overordering_min = overordering_min self.overordering_exp = overordering_exp self.mismatch_exp = mismatch_exp self.mismatch_max = mismatch_max super().__init__(*args, **kwargs)
[docs] def init(self): if 0 < self.mismatch_max < 1: self.mismatch_max *= self.awi.n_lines return super().init()
[docs] def distribute_needs(self, t: float) -> dict[str, int]: """Distributes my needs randomly over all my partners""" dist = dict() for needs, all_partners in [ (self.awi.needed_supplies, self.awi.my_suppliers), (self.awi.needed_sales, self.awi.my_consumers), ]: # find suppliers and consumers still negotiating with me partners = [_ for _ in all_partners if _ in self.negotiators.keys()] n_partners = len(partners) # if I need nothing, end all negotiations if needs <= 0: dist.update(dict(zip(partners, [0] * n_partners))) continue # distribute my needs over my (remaining) partners. dist.update( dict( zip( partners, distribute( int(needs * (1 + self._overordering_fraction(t))), n_partners, equal=self.equal_distribution, allow_zero=self.awi.allow_zero_quantity, ), ) ) ) return dist
[docs] def first_proposals(self): # just randomly distribute my needs over my partners (with best price for me). s, p = self._step_and_price(best_price=True) distribution = self.distribute_needs(t=0) d = { k: (q, s, p) if q > 0 or self.awi.allow_zero_quantity else None for k, q in distribution.items() } return d
[docs] def counter_all(self, offers, states): response = dict() future_partners = { k for k, v in offers.items() if v[TIME] != self.awi.current_step } offers = {k: v for k, v in offers.items() if v[TIME] == self.awi.current_step} # process for sales and supplies independently for needs, all_partners, issues in [ ( self.awi.needed_supplies, self.awi.my_suppliers, self.awi.current_input_issues, ), ( self.awi.needed_sales, self.awi.my_consumers, self.awi.current_output_issues, ), ]: # get a random price price = issues[UNIT_PRICE].rand() # find active partners in some random order partners = [_ for _ in all_partners if _ in offers.keys()] random.shuffle(partners) partners = set(partners) # find the set of partners that gave me the best offer set # (i.e. total quantity nearest to my needs) plist = list(powerset(partners))[::-1] best_diff, best_indx = float("inf"), -1 for i, partner_ids in enumerate(plist): offered = sum(offers[p][QUANTITY] for p in partner_ids) diff = abs(offered - needs) if diff < best_diff: best_diff, best_indx = diff, i if diff == 0: break unneeded_response = ( SAOResponse(ResponseType.END_NEGOTIATION, None) if not self.awi.allow_zero_quantity else SAOResponse( ResponseType.REJECT_OFFER, (0, self.awi.current_step, 0) ) ) # If the best combination of offers is good enough, accept them and end all # other negotiations th = self._allowed_mismatch(min(_.relative_time for _ in states.values())) if best_diff <= th: partner_ids = plist[best_indx] others = list(partners.difference(partner_ids).union(future_partners)) response |= { k: SAOResponse(ResponseType.ACCEPT_OFFER, offers[k]) for k in partner_ids } | {k: unneeded_response for k in others} continue # If I still do not have a good enough offer, distribute my current needs # randomly over my partners. t = min(_.relative_time for _ in states.values()) distribution = self.distribute_needs(t) response.update( { k: ( unneeded_response if q == 0 else SAOResponse( ResponseType.REJECT_OFFER, (q, self.awi.current_step, price) ) ) for k, q in distribution.items() } ) return response
[docs] def _allowed_mismatch(self, r: float): mn, mx = 0, self.mismatch_max return mn + (mx - mn) * (r**self.mismatch_exp)
[docs] def _overordering_fraction(self, t: float): mn, mx = self.overordering_min, self.overordering_max return mx - (mx - mn) * (t**self.overordering_exp)
[docs] def _step_and_price(self, best_price=False): """Returns current step and a random (or max) price""" s = self.awi.current_step seller = self.awi.is_first_level issues = ( self.awi.current_output_issues if seller else self.awi.current_input_issues ) pmin = issues[UNIT_PRICE].min_value pmax = issues[UNIT_PRICE].max_value if best_price: return s, pmax if seller else pmin return s, random.randint(pmin, pmax)
[docs] class RandDistOneShotAgent(SyncRandomOneShotAgent): """ An agent that distributes its needs over its partners randomly. Args: equal: If given, it tries to equally distribute its needs over as many of its suppliers/consumers as possible overordering_max: Maximum fraction of needs to over-order. For example, it the agent needs 5 items and this is 0.2, it will order 6 in the first negotiation step. overordering_min: Minimum fraction of needs to over-order. Used in the last negotiation step. overordering_exp: Controls how fast does the over-ordering quantity go from max to min. concession_exp: Controls how fast does the agent concedes on matching its needs exactly. mismatch_max: Maximum mismtach in quantity allowed between needs and accepted offers. If a fraction, it is will be this fraction of the production capacity (n_lines). """ def __init__(self, *args, **kwargs): kwargs["equal"] = False super().__init__(*args, **kwargs)
[docs] class EqualDistOneShotAgent(SyncRandomOneShotAgent): """Same as RandDistOneShotAgent but defaulting to equal distribution of needs Args: equal: If given, it tries to equally distribute its needs over as many of its suppliers/consumers as possible overordering_max: Maximum fraction of needs to over-order. For example, it the agent needs 5 items and this is 0.2, it will order 6 in the first negotiation step. overordering_min: Minimum fraction of needs to over-order. Used in the last negotiation step. overordering_exp: Controls how fast does the over-ordering quantity go from max to min. concession_exp: Controls how fast does the agent concedes on matching its needs exactly. mismatch_max: Maximum mismtach in quantity allowed between needs and accepted offers. If a fraction, it is will be this fraction of the production capacity (n_lines). """ def __init__(self, *args, **kwargs): kwargs["equal"] = True super().__init__(*args, **kwargs)
[docs] class SingleAgreementRandomAgent(OneShotSingleAgreementAgent): """A controller that agrees randomly to one offer""" def __init__(self, *args, p_accept: float = PROB_ACCEPTANCE, **kwargs): super().__init__(*args, **kwargs) self._p_accept = p_accept
[docs] def is_acceptable(self, offer: Outcome, source: str, state: SAOState) -> bool: return random.random() < self._p_accept
[docs] def best_offer(self, offers: dict[str, Outcome]) -> str | None: return random.choice(list(offers.keys()))
[docs] def is_better( self, a: Outcome | None, b: Outcome | None, negotiator: str, state: SAOState ) -> bool: return random.random() < 0.5