Running a Negotiation

NegMAS has several built-in negotiation Mechanism s (Protocol s), negotiation agents (Negotiator s), and UtilityFunction s. You can use these to run negotiations as follows:

import random # for generating random ufuns
random.seed(0) # for reproducibility
from pprint import pprint # for printing
from negmas import SAOMechanism, AspirationNegotiator, MappingUtilityFunction

session = SAOMechanism(outcomes=10, n_steps=100)
negotiators = [AspirationNegotiator(name=f'a{_}') for _ in range(5)]
for negotiator in negotiators:
    session.add(negotiator, ufun=MappingUtilityFunction(lambda x: random.random() * x[0]))

pprint(session.run().__dict__)
{'agreement': (9,),
 'broken': False,
 'current_offer': (9,),
 'current_proposer': 'a3-079be4c1-5322-4ef5-b664-307337d4e252',
 'error_details': '',
 'has_error': False,
 'n_acceptances': 0,
 'n_negotiators': 5,
 'new_offers': [],
 'relative_time': 0.09,
 'results': None,
 'running': False,
 'started': True,
 'step': 8,
 'time': 0.004660927999999842,
 'timedout': False,
 'waiting': False}

Negotations end with a status that shows you what happens. In the above example, we can see that the negotiation was not broken and did not time-out. The agreement was on outcome (4,) of the 10 possible outcomes of this negotiation. That offer was offered by negotiator a0 (the rest of the agent ID is always a random value to ensure no name repetitions) which was accepted by all of the other 4 negotiators.

It is possible to run the same negotiation using a Protocol object instead of a Mechanism object

random.seed(0) # for reproducibility
from negmas import SAOProtocol

session = SAOProtocol(outcomes=10, n_steps=100)
negotiators = [AspirationNegotiator(name=f'a{_}') for _ in range(5)]
for negotiator in negotiators:
    session.add(negotiator, ufun=MappingUtilityFunction(lambda x: random.random() * x[0]))

pprint(session.run().__dict__)
{'agreement': (9,),
 'broken': False,
 'current_offer': (9,),
 'current_proposer': 'a3-107c35bf-283d-49b2-8b1b-650e253df20c',
 'error_details': '',
 'has_error': False,
 'n_acceptances': 0,
 'n_negotiators': 5,
 'new_offers': [],
 'relative_time': 0.09,
 'results': None,
 'running': False,
 'started': True,
 'step': 8,
 'time': 0.005280838000000898,
 'timedout': False,
 'waiting': False}

As you can see, we got the same output.Protocol is an alias of Mechanism in NegMAS.

Let’s try a more meaningful situation: Assume we have a buyer and a seller who are negotiating about a business transaction in which the buyer wants to maximize his profit while the seller wants to minimize her cost. They both would like to transact on as much as possible of the product and each has some preferred delivery time.

This can be modeled in the following negotiation:

from negmas import Issue, SAOMechanism, AspirationNegotiator, normalize
from negmas.utilities import LinearUtilityAggregationFunction as LUFun
issues = [Issue(name='price', values=10), Issue(name='quantity', values=10)
          , Issue(name='delivery_time', values=10)]
session = SAOMechanism(issues=issues, n_steps=20)

buyer_utility = normalize(ufun=LUFun(issue_utilities={'price': lambda x: 9.0 - x
                                       , 'quantity': lambda x: 0.2 * x
                                       , 'delivery_time': lambda x: x})
                         , outcomes=session.outcomes)

seller_utility = normalize(ufun=LUFun(issue_utilities={'price': lambda x: x
                                       , 'quantity': lambda x: 0.2 * x
                                       , 'delivery_time': lambda x: 9.0 - x})
                           , outcomes=session.outcomes)


session.add(AspirationNegotiator(name='buyer'), ufun=buyer_utility)
session.add(AspirationNegotiator(name='seller'), ufun=seller_utility)
pprint(session.run().__dict__)
{'agreement': (9, 9, 8),
 'broken': False,
 'current_offer': (9, 9, 8),
 'current_proposer': 'seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0',
 'error_details': '',
 'has_error': False,
 'n_acceptances': 0,
 'n_negotiators': 2,
 'new_offers': [],
 'relative_time': 0.9,
 'results': None,
 'running': False,
 'started': True,
 'step': 17,
 'time': 0.003675460000000186,
 'timedout': False,
 'waiting': False}

In this run, we can see that the agreement was on a high price (9) which is preferred by the seller but with a delivery time of 8 which is preferred by the buyer. Negotiation took 17 steps out of the allowed 20 (90% of the available time)

We can check the negotiation history as well

for i, _ in enumerate(session.history):
    print(f'{i:03}: {_.new_offers}')
000: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 0))]
001: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 0))]
002: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 0))]
003: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 0))]
004: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 0))]
005: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 0))]
006: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 8, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 8, 0))]
007: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 7, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 7, 0))]
008: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (0, 5, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 5, 0))]
009: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (1, 8, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 8, 1))]
010: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (1, 5, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 5, 1))]
011: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (2, 7, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 7, 2))]
012: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (3, 7, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 7, 3))]
013: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (4, 6, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 6, 4))]
014: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (6, 8, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 8, 6))]
015: [('buyer-521a6d50-9468-48ec-ae4d-548b52bd8849', (8, 9, 9)), ('seller-7ddbb9cf-1d27-47b0-98f5-66a480ba52b0', (9, 9, 8))]
016: []

We can even plot the complete negotiation history and visually see how far were the result from the pareto frontier (it was 0.0 utility units far from it).

session.plot(plot_outcomes=False)
../_images/01.running_simple_negotiation_10_0.png

What happens if the seller was much more interested in delivery time.

Firstly, what do you expect?

Given that delivery time becomes a more important issue now, the seller will get more utility points by allowing the price to go down given that the delivery time can be made earlier. This means that we should expect the delivery time and price to go down. Let’s see what happens:

session = SAOMechanism(issues=issues, n_steps=50)

seller_utility = normalize(ufun=LUFun(issue_utilities={'price': lambda x: x
                                       , 'quantity': lambda x: 0.2 * x
                                       , 'delivery_time': lambda x: 9.0 - x}
                                     , weights = {'price': 1.0, 'quantity': 1.0, 'delivery_time': 10.0})
                         , outcomes=session.outcomes)

session.add(AspirationNegotiator(name='buyer'), ufun=buyer_utility)
session.add(AspirationNegotiator(name='seller'), ufun=seller_utility)
pprint(session.run().__dict__)
{'agreement': (5, 4, 5),
 'broken': False,
 'current_offer': (5, 4, 5),
 'current_proposer': 'seller-49534369-8d93-42b7-b34e-b3f33083bc6d',
 'error_details': '',
 'has_error': False,
 'n_acceptances': 0,
 'n_negotiators': 2,
 'new_offers': [],
 'relative_time': 0.9,
 'results': None,
 'running': False,
 'started': True,
 'step': 44,
 'time': 0.010532425000000956,
 'timedout': False,
 'waiting': False}

We can check it visually as well:

session.plot(plot_outcomes=False)
../_images/01.running_simple_negotiation_14_0.png

It is clear that the new ufuns transformed the problem. Now we have a single outcome at the pareto front. Nevertheless, there is money on the table as the negotiators did not agree on an outcome on the pareto front.

What happens if we give them more time to negotiate:

session = SAOMechanism(issues=issues, n_steps=5000)

seller_utility = normalize(ufun=LUFun(issue_utilities={'price': lambda x: x
                                       , 'quantity': lambda x: 0.2 * x
                                       , 'delivery_time': lambda x: 9.0 - x}
                                     , weights = {'price': 1.0, 'quantity': 1.0, 'delivery_time': 10.0})
                         , outcomes=session.outcomes)

session.add(AspirationNegotiator(name='buyer'), ufun=buyer_utility)
session.add(AspirationNegotiator(name='seller'), ufun=seller_utility)
session.run()
session.plot(plot_outcomes=False)
../_images/01.running_simple_negotiation_16_0.png

It did not help much! The two agents adjusted their concession to match the new time and they did not get to the Pareto-front.

Let’s allow them to concede faster by setting their aspiration_type to linear instead of the default boulware:

session = SAOMechanism(issues=issues, n_steps=5000)

seller_utility = normalize(ufun=LUFun(issue_utilities={'price': lambda x: x
                                       , 'quantity': lambda x: 0.2 * x
                                       , 'delivery_time': lambda x: 9.0 - x}
                                     , weights = {'price': 1.0, 'quantity': 1.0, 'delivery_time': 10.0})
                         , outcomes=session.outcomes)

session.add(AspirationNegotiator(name='buyer', aspiration_type="linear"), ufun=buyer_utility)
session.add(AspirationNegotiator(name='seller', aspiration_type="linear"), ufun=seller_utility)
session.run()
session.plot(plot_outcomes=False)
../_images/01.running_simple_negotiation_18_0.png

It is clear that longer negotiation time, and faster concession did not help the negotiators get to a point on the pareto-front.

Download Notebook.