Running Controlled Experiments
------------------------------
The simplest way to compare different agent designs is to run
tournaments between them as explained in details in previous tutorials.
Nevertheless, while developing an agent, it is beneficial to be able to
run experiments in which you specify **exactly** some or all factors
that may affect agent performance in order to analyze its behavior.
This tutorial will explain how to achieve this at different levels of
control. As expected with more control comes more responsibility (and
complexity). For this tutorial we will use the SCMLOneshot game but
everthing we discuss applies exactly to all SCML environments. Moreover,
we will use builtin agents for illustration but of course the same
methods apply to any agents.
The recommended method
~~~~~~~~~~~~~~~~~~~~~~
The recommended method is to create a ``Context`` that exactly defines
the conditions of your experiment. SCML provides a large number of
contexts for oneshot and std (Search `SCML
documentation `__ for Context to
learn more). For example, ``SupplierContext`` will create contexts in
which your agent is always a supplier and so on. You can create new
contexts as well. The context that allows you to control exactly what
happens is the ``RepeatingContext`` which receives some predefined
configurations (may be generated through the ``generate()`` method
described leater or defined explicitly) and just repeats them. This
gives you the finest possible control over the experiment. In the
remaining of this tutorial we focus on using ``generate()`` to create
worlds directly but you can also use the generated configurations in a
repeating context.
Levels of control
~~~~~~~~~~~~~~~~~
There are four levels of control for world generation:
1. The tournament level: In this case you use ``negmas.tournament`` or —
more conveniently — ``scml.utils.anac2022_*`` functions to run a
tournament. You can adjust the parameters used to create worlds in
this simulation using almost all the same parameters that are
received by ``SCML2024OneShotWorld.generate()`` method (next level of
control below). It is harder in this case to control the number of
simulations and factory assignment.
2. You can use ``OneShotWorld.generate()`` or ``StdWorld.generate()``
method to generate a **single** world for which you can either allow
the system to select agent types and their placement or with full
control over those. Using this approach (recommended), you can
control almost all aspects of the simulations except the **exact**
exogenous contracts and profiles for agents.
3. You can just construct ``OneShotWorld`` and ``StdWorld`` objects
directly which allows you to control everything including every
single profile and exogenous contract.
4. You can subclass the world and override some of its private members
to modify how the simulation runs. For example, you can override
``_make_issues()`` to change the ranges of issues for negotiations.
This tutorial will focus on the second approach and will touch upon the
third.
Using ``generate()``:
~~~~~~~~~~~~~~~~~~~~~
There are two general ways to use the ``generate()`` method of SCML
worlds for world generation:
1. You pass ``random_agent_types=True``: In this case, you need only
pass ``agent_types`` and the system will generate random simulations
using these types. If you want a specific type to be more represented
than others, just repeat it in the ``agent_types`` list. You can also
pass ``n_agents_per_process`` to control how many agents are present
on each production level but you cannot control the exact type of
each one of them.
2. You pass ``random_agent_types=False``: In this case, you need to pass
``agent_types`` and ``agent_processes`` (``n_agents_per_process``
will be ignored). The former gives the type of each single agent in
the simulation and the later gives the corresponding production
level. This gives you full control over the type of agent controlling
every single factory in the simulation.
We will see examples of both of these approaches in this tutorial. Let’s
first import in what we need:
.. code:: ipython3
from scml.oneshot.world import SCML2023OneShotWorld
from scml.oneshot.world import is_system_agent
from scml.oneshot.agents import GreedyOneShotAgent, GreedySingleAgreementAgent
Controlling agent allocation to factories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can control which agents are allocated to which factory by using the
``generate()`` method of the ``SCML2020OneShotWorld`` class (or any SCML
world class). Let’s say that you want to have :math:`10` agents with the
first two of type ``GreedyOneShotAgent`` and the rest of the type
``GreedySingleAgreementAgentAgent``, here is a simple way to achieve
that:
.. code:: ipython3
types = [GreedyOneShotAgent] * 2 + [GreedySingleAgreementAgent] * 8
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
agent_processes=[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
n_processes=2,
n_steps=5,
construct_graphs=True,
random_agent_types=False,
)
)
world.draw(what=["contracts-concluded"])
plt.show()
world.run()
plt.show()
.. image:: 05.experiments_files/05.experiments_3_0.png
as you can see, the first two agents (“01Gre@0” and “00Gre@0”) are of
type ``GreedyOneshotAgent`` and the rest are of type
``GreedySingleAgreementAgent``. That is clear from the naming convention
of putting the first :math:`3` letters of the type before the ``@`` sign
in the agent name. You can confirm it explicity by checking types:
.. code:: ipython3
[a._obj.__class__.__name__ for a in world.agents.values() if not is_system_agent(a.id)]
.. parsed-literal::
['GreedyOneShotAgent',
'GreedyOneShotAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent',
'GreedySingleAgreementAgent']
What happens if we want to create a world in which the number of agents
at every level are different. Let’s first try just extending the
approach we used before:
.. code:: ipython3
types = [GreedyOneShotAgent] * 2 + [GreedySingleAgreementAgent] * 8
fig, axs = plt.subplots(1, 4)
for ax in axs:
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
n_agents_per_process=(3, 7),
n_processes=2,
n_steps=10,
construct_graphs=True,
)
)
world.draw(axs=ax, steps=(0, world.n_steps), what=["contracts-concluded"])
.. image:: 05.experiments_files/05.experiments_7_0.png
We can run the last of these worlds just to be sure something happens!!
.. code:: ipython3
world.run()
world.draw(what=["contracts-concluded"], steps=(0, world.n_steps - 1));
.. image:: 05.experiments_files/05.experiments_9_0.png
As you can see, passing a tuple as ``n_agents_per_process`` did not
help. We generated two world. They were different and neither had the
distribution we wanted. That is because in this case, the generator will
be guaranteed to make a world in which the number of agents in every
level is **between 3 and 7** not exactly either of them.
.. code:: ipython3
types = [GreedyOneShotAgent] * 2 + [GreedySingleAgreementAgent] * 8
fig, axs = plt.subplots(1, 4)
for ax in axs:
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
n_agents_per_process=[3, 7],
n_processes=2,
n_steps=5,
construct_graphs=True,
)
)
world.draw(axs=ax, what=["contracts-concluded"])
.. image:: 05.experiments_files/05.experiments_11_0.png
That works. We can also use it to generate deeper graphs of our
choosing:
.. code:: ipython3
types = [GreedyOneShotAgent] * 2 + [GreedySingleAgreementAgent] * 8
agents_per_process = [2, 3, 2, 3]
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
n_agents_per_process=agents_per_process,
n_processes=len(agents_per_process),
n_steps=5,
construct_graphs=True,
random_agent_types=False,
)
)
world.draw(what=["contracts-concluded"])
plt.show()
.. image:: 05.experiments_files/05.experiments_13_0.png
Exactly what the doctors ordered!
Controlling construction paramteres
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We have seen now that you can control the agent types. What about having
agents of the same type but with different prarameters? You will need to
do that for example if you want to compare different options for the
initialization parameters of your agent. Because of a technical
difference between the implementations of SCMLOneshot and standard SCML
game, it is much easier to see what is going on in the ``SCML2021World``
case.
In this case, we can use the ``agent_params`` input to ``generate()`` as
follows:
.. code:: ipython3
from scml.scml2020.world import SCML2023World
from scml.scml2020.agents import DecentralizingAgent, BuyCheapSellExpensiveAgent
from negmas import NaiveTitForTatNegotiator
types = [DecentralizingAgent] * 2 + [BuyCheapSellExpensiveAgent] * 4
params = [dict(negotiator_type=NaiveTitForTatNegotiator), dict()] + [dict()] * 4
agents_per_process = [2, 3, 1]
world = SCML2023World(
**SCML2023World.generate(
agent_types=types,
agent_params=params,
n_agents_per_process=agents_per_process,
n_processes=len(agents_per_process),
n_steps=5,
construct_graphs=True,
random_agent_types=False,
)
)
world.draw(what=["contracts-concluded"])
plt.show()
.. image:: 05.experiments_files/05.experiments_15_0.png
By just looking at the graph, we cannot be sure about what happened.
Nevertheless, we can still check the construction parameters from the
world itself:
.. code:: ipython3
print(world.agent_params[:-2])
.. parsed-literal::
[{}, {}, {}, {}, {}, {}]
We can see that the first agent had the negotiator-type we asked for and
the rest are just getting their default initialization paramters.
For SCMLOneshot agents, the approach is slightly different due to the
fact that the ``OneShotAgent`` is actually a ``Controller`` not an
``Agent`` in NegMAS’s parallance. The exact meaning of this is not
relevant for our current discussion though. What we care about is
creating agents with controlled construction paramters. Let’s try the
same method:
.. code:: ipython3
types = [GreedyOneShotAgent] * 2 + [GreedySingleAgreementAgent] * 4
params = [
dict(controller_params=dict(concession_exponent=0.4)),
dict(controller_params=dict(concession_exponent=3.0)),
] + [dict()] * 4
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
agent_params=params,
agent_processes=[0, 0, 1, 1, 1, 2],
n_processes=3,
n_steps=5,
construct_graphs=True,
random_agent_types=False,
)
)
world.draw(what=["contracts-concluded"])
plt.show()
.. image:: 05.experiments_files/05.experiments_19_0.png
Firstly, note that, in this case, we needed to encolose our paramters
dict within another dict and pass it to the key ``controller_params``.
That is necessary as these paramters are not to be passed to the
adapther used to run the agent within SCMLOneshot but to our agent which
is the controller.
How can we check that it worked? Let’s first try doing the same thing we
did before and examing ``agent_params`` of the ``world``:
.. code:: ipython3
print(world.agent_params[:-2])
.. parsed-literal::
[{}, {}, {}, {}, {}, {}]
No … definitely not. The reason is that these are the paramters of the
adapter not our controller. To confirm that the concession rate was
passed correctly to our agents, we need to check them directly as
follows:
.. code:: ipython3
for a in list(world.agents.values())[:2]:
print(a._obj._e)
.. parsed-literal::
0.4
3.0
Yes. That is what we expected. The first two agents have the concession
exponents we passed to them.
Controlling other aspects of the simulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can control other aspects of the simulation by passing specific
paramters to the
`generate() `__
method or the `World
constructor `__
directly.
Here is an example in which we use ``generate()`` and fix the inital
balance of all agents to :math:`1000` while fixing the production cost
of everyone to :math:`20`, increasing the number of production lines to
:math:`20`, and setting the number of simulation steps (days) to
:math:`40` while making all negotiations go for :math:`100` steps
instead of :math:`20` keeping the number of negotiation steps per day at
:math:`101` which means that negotiations are still guaranteed to finish
within the same day in which they are started. This configuration is
very different than the one used by default in the official competition
but you can decide to test it:
.. code:: ipython3
types = [GreedyOneShotAgent] * 7
agents_per_process = [2, 3, 2]
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
n_agents_per_process=agents_per_process,
n_processes=len(agents_per_process),
n_steps=20,
neg_n_steps=100,
production_costs=50,
cost_increases_with_level=False,
initial_balance=1000,
construct_graphs=True,
random_agent_types=False,
)
)
world.draw(what=["contracts-concluded"])
plt.show()
.. image:: 05.experiments_files/05.experiments_26_0.png
It is easy enough to check that some of these paramters are correct. For
example:
.. code:: ipython3
world.neg_n_steps
.. parsed-literal::
100
As expected. Checking the initial balances and production costs is
harder. Let’s look at the initial balances:
.. code:: ipython3
for a in world.agents.values():
if is_system_agent(a.id):
continue
print(f"{a.id} -> {a.awi.current_balance}")
.. parsed-literal::
00Gr@0 -> 1000
01Gr@0 -> 1000
02Gr@1 -> 1000
03Gr@1 -> 1000
04Gr@1 -> 1000
05Gr@2 -> 1000
06Gr@2 -> 1000
As expected again. What about production cost?
.. code:: ipython3
for a in world.agents.values():
if is_system_agent(a.id):
continue
print(f"{a.id} -> {a.awi.profile.cost}")
.. parsed-literal::
00Gr@0 -> 50
01Gr@0 -> 50
02Gr@1 -> 50
03Gr@1 -> 50
04Gr@1 -> 50
05Gr@2 -> 50
06Gr@2 -> 50
This time, we will run this world to just see that it still works after
all of this mingling:
.. code:: ipython3
world.run()
world.draw(what=["contracts-concluded"], steps=(0, world.n_steps))
plt.show()
.. image:: 05.experiments_files/05.experiments_34_0.png
Seems fine.
Controlling Profiles
~~~~~~~~~~~~~~~~~~~~
In the previous example, we used ``generae()`` to do our bidding instead
of directly calling the world constructore. Why? The main reason is that
``generate()`` creates profiles and exogenous contracts compatible with
our settings so that it is possible — in principly — to make money in
the generated world. Moreover, this is controllable by its parameters
(see ``profit_*`` parameters
`here `__).
We can push things a little further by controlling the profile of each
agent independently (which in this case is just its production cost). We
will generate a world in which agents have costs from :math:`1` to
:math:`7`.
.. code:: ipython3
types = [GreedyOneShotAgent] * 7
agents_per_process = [2, 3, 2]
world = SCML2023OneShotWorld(
**SCML2023OneShotWorld.generate(
agent_types=types,
n_agents_per_process=agents_per_process,
n_processes=len(agents_per_process),
production_costs=list(range(1, 8)),
cost_increases_with_level=False,
construct_graphs=True,
random_agent_types=False,
)
)
world.draw(what=["contracts-concluded"])
plt.show()
.. image:: 05.experiments_files/05.experiments_37_0.png
Let’s now check the production costs:
.. code:: ipython3
for a in world.agents.values():
if is_system_agent(a.id):
continue
print(f"{a.id} -> {a.awi.profile.cost}")
.. parsed-literal::
00Gr@0 -> 1
01Gr@0 -> 2
02Gr@1 -> 3
03Gr@1 -> 4
04Gr@1 -> 5
05Gr@2 -> 6
06Gr@2 -> 7
It is crucial here that we passed ``cost_increases_with_level=False``,
otherwise, the system will just increase the costs of agents in the
second and third production levels.
The disadvantage of this approach is that you cannot control **exactly**
the exogenous contracts. These are generated by the ``generate()``
method for us. To control this final piece of the world, we need to
directly call the world constructor. We will see now how to do that for
both types of SCML worlds.
Controlling exogenous contracts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here we cannot use the ``generate()`` method and must call the world
constructor directly. This is the most complex approach as we need to
set everything up exactly right.
Standard SCML2023World
^^^^^^^^^^^^^^^^^^^^^^
Let’s try to do it for the ``SCML2023World`` first:
.. code:: ipython3
import numpy as np
from scml.scml2020.common import FactoryProfile
from scml.scml2020.common import INFINITE_COST, ExogenousContract
types = [DecentralizingAgent] * 3
agents_per_process = [2, 1]
n_processes = len(agents_per_process)
n_lines = 10
# setup the factory profiles. For each factory we
# set production cost to INFINITE_COST for all processes
# except the one it can actually run
profiles = [
FactoryProfile(np.asarray([[3, INFINITE_COST]] * n_lines)),
FactoryProfile(np.asarray([[20, INFINITE_COST]] * n_lines)),
FactoryProfile(np.asarray([[INFINITE_COST, 5]] * n_lines)),
]
# create exogenous contracts
exogenous = [
## exogenous supply
ExogenousContract(
product=0,
quantity=10,
unit_price=5,
time=1,
revelation_time=1,
seller=-1,
buyer=0,
),
ExogenousContract(
product=0,
quantity=10,
unit_price=7,
time=2,
revelation_time=0,
seller=-1,
buyer=0,
),
## exogenous sales
ExogenousContract(
product=0,
quantity=10,
unit_price=5,
time=1,
revelation_time=0,
seller=2,
buyer=-1,
),
]
world = SCML2023World(
process_inputs=np.ones(n_processes),
process_outputs=np.ones(n_processes),
catalog_prices=[10, 20, 30],
profiles=profiles,
agent_types=types,
agent_params=[dict()] * 3,
exogenous_contracts=exogenous,
n_steps=5,
construct_graphs=True,
agent_name_reveals_position=True,
agent_name_reveals_type=True,
)
world.draw(what=["contracts-concluded"])
plt.show()
.. image:: 05.experiments_files/05.experiments_42_0.png
Let’s check the exogenous contracts in the system then explain what just
happened:
.. code:: ipython3
from pprint import pprint
pprint(
list(
(
list(str(_) for _ in contracts)
for s, contracts in world.exogenous_contracts.items()
)
)
)
.. parsed-literal::
[["Contract(agreement={'time': 1, 'quantity': 10, 'unit_price': 5}, "
"partners=('00De@0', 'SELLER'), annotation={'seller': 'SELLER', 'buyer': "
"'00De@0', 'caller': 'SELLER', 'is_buy': False, 'product': 0}, issues=(), "
'signed_at=-1, executed_at=-1, concluded_at=-1, nullified_at=-1, '
'to_be_signed_at=1, signatures={}, mechanism_state=None, mechanism_id=None, '
"id='fe0624eb-8cfa-468f-affb-4bdcd88f451f')"],
["Contract(agreement={'time': 2, 'quantity': 10, 'unit_price': 7}, "
"partners=('00De@0', 'SELLER'), annotation={'seller': 'SELLER', 'buyer': "
"'00De@0', 'caller': 'SELLER', 'is_buy': True, 'product': 0}, issues=(), "
'signed_at=-1, executed_at=-1, concluded_at=-1, nullified_at=-1, '
'to_be_signed_at=0, signatures={}, mechanism_state=None, mechanism_id=None, '
"id='50763d3c-b112-4b57-b1d9-b29513fb7444')",
"Contract(agreement={'time': 1, 'quantity': 10, 'unit_price': 5}, "
"partners=('BUYER', '02De@1'), annotation={'seller': '02De@1', 'buyer': "
"'BUYER', 'caller': 'BUYER', 'is_buy': True, 'product': 0}, issues=(), "
'signed_at=-1, executed_at=-1, concluded_at=-1, nullified_at=-1, '
'to_be_signed_at=0, signatures={}, mechanism_state=None, mechanism_id=None, '
"id='33a6bda4-b143-4d91-94c5-7de5f9749c39')"],
[],
[],
[]]
You can confirm for yourself that this is exactly what we expected.
Let’s first discuss the profile. In ``SCML2021World``, an agent’s
profile consists of the production cost **per line per product**. You
can see the full definition
`here `__.
That is why we needed to create a 2D array of costs.
Exogenous contract structure is self explanatory. You have to specify
the product, delivery time, quantity, and unit price. Moreover, you have
to specify the time at which this contract is revealed to its agent
(which must be before or at the delivery time step). The one thing you
should be careful about is setting the *buyer* to :math:`-1` for
exogenous sales and the *seller* to :math:`-1` for exogenous supplies.
You can in principle have exogenous contracts in the middle of the chain
but we do not do that usually.
Let’s try to run this world
.. code:: ipython3
world.run()
_, axs = plt.subplots(2)
world.draw(
what=["negotiations-started", "contracts-concluded"],
steps=(0, world.n_steps),
together=False,
axs=axs,
)
plt.show()
::
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/code/projects/negmas/negmas/situated/world.py:1557, in World._step_a_mechanism(self, mechanism, force_immediate_signing, action)
1556 try:
-> 1557 result = mechanism.step(action)
1558 except Exception as e:
File ~/code/projects/negmas/negmas/mechanisms.py:1114, in Mechanism.step(self, action)
1113 strt = time.perf_counter()
-> 1114 a._on_negotiation_start(state=state)
1115 self._negotiator_times[a.id] += time.perf_counter() - strt
File ~/code/projects/negmas/negmas/negotiators/negotiator.py:301, in Negotiator._on_negotiation_start(self, state)
300 super().set_preferences(self._preferences, force=True)
--> 301 self.on_negotiation_start(state=state)
File ~/code/projects/negmas/negmas/negotiators/controller.py:411, in Controller.on_negotiation_start(self, negotiator_id, state)
410 if negotiator is None:
--> 411 raise ValueError(f"Unknown negotiator {negotiator_id}")
412 return self.call(negotiator, "on_negotiation_start", state=state)
ValueError: Unknown negotiator 00De@0
During handling of the above exception, another exception occurred:
ValueError Traceback (most recent call last)
Cell In[23], line 1
----> 1 world.run()
3 _, axs = plt.subplots(2)
4 world.draw(
5 what=["negotiations-started", "contracts-concluded"],
6 steps=(0, world.n_steps),
7 together=False,
8 axs=axs,
9 )
File ~/code/projects/negmas/negmas/situated/world.py:2280, in World.run(self)
2278 if self.time >= self.time_limit:
2279 break
-> 2280 if not self.step():
2281 break
File ~/code/projects/negmas/negmas/situated/world.py:1880, in World.step(self, n_neg_steps, n_mechanisms, actions, neg_actions)
1878 assert self.__next_operation_index != 0
1879 while self.__next_operation_index != 0:
-> 1880 if not _negotiate(n_neg_steps):
1881 pass
1882 # print(
1883 # "Some negotiations are still running but all should be completed by now"
1884 # )
File ~/code/projects/negmas/negmas/situated/world.py:1800, in World.step.._negotiate(n_steps_to_run)
1791 if n_mechanisms is not None and len(mechanisms) > n_mechanisms:
1792 mechanisms = mechanisms[:n_mechanisms]
1793 (
1794 _,
1795 _,
1796 n_steps_broken_,
1797 n_steps_success_,
1798 n_broken_,
1799 n_success_,
-> 1800 ) = self._step_negotiations(
1801 [_[0] for _ in mechanisms],
1802 n_steps_to_run,
1803 False,
1804 [_[1] for _ in mechanisms],
1805 action=neg_actions,
1806 )
1807 self.__stepped_mechanisms = self.__stepped_mechanisms.union(
1808 {_[0].id for _ in mechanisms}
1809 )
1810 running = [
1811 _.mechanism.id
1812 for _ in self._negotiations.values()
(...)
1815 and not _.mechanism.state.ended
1816 ]
File ~/code/projects/negmas/negmas/situated/world.py:1648, in World._step_negotiations(self, mechanisms, n_steps, force_immediate_signing, partners, action)
1646 break
1647 mechanism = mechanisms[i]
-> 1648 contract, r = self._step_a_mechanism(
1649 mechanism,
1650 force_immediate_signing,
1651 action=action.get(mechanism.id, None) if action else None,
1652 )
1653 contracts[i] = contract
1654 running[i] = r
File ~/code/projects/negmas/negmas/situated/world.py:1559, in World._step_a_mechanism(self, mechanism, force_immediate_signing, action)
1557 result = mechanism.step(action)
1558 except Exception as e:
-> 1559 result = mechanism.abort()
1560 if not self.ignore_negotiation_exceptions:
1561 raise e
File ~/code/projects/negmas/negmas/mechanisms.py:1228, in Mechanism.abort(self)
1222 """Aborts the negotiation."""
1223 (
1224 self._current_state.has_error,
1225 self._current_state.error_details,
1226 self._current_state.waiting,
1227 ) = (True, "Uncaught Exception", False)
-> 1228 self.on_mechanism_error()
1229 (
1230 self._current_state.broken,
1231 self._current_state.timedout,
1232 self._current_state.agreement,
1233 ) = (True, False, None)
1234 state = self.state
File ~/code/projects/negmas/negmas/mechanisms.py:945, in Mechanism.on_mechanism_error(self)
943 for a in self.negotiators:
944 strt = time.perf_counter()
--> 945 a.on_mechanism_error(state=state)
946 self._negotiator_times[a.id] += time.perf_counter() - strt
File ~/code/projects/negmas/negmas/negotiators/controller.py:474, in Controller.on_mechanism_error(self, negotiator_id, state)
472 negotiator, cntxt = self._negotiators.get(negotiator_id, (None, None))
473 if negotiator is None:
--> 474 raise ValueError(f"Unknown negotiator {negotiator_id}")
475 return self.call(negotiator, "on_mechanism_error", state=state)
ValueError: Unknown negotiator 00De@0
We can see that there were :math:`2` concluded exogenous supply
contracts and :math:`1` concluded exogenous sale contracts. We can also
see that there were :math:`7` negotiations in total in this world none
of them leading to contracts.
SCMLOneshot World
^^^^^^^^^^^^^^^^^
The situation is slightly different for the SCMLOneshot world just
because the format of the profile and exogenous contract data structures
is slightly different. Here is an example case:
.. code:: ipython3
import numpy as np
from scml.oneshot import OneShotProfile
from scml.oneshot import OneShotExogenousContract
from scml.oneshot import DefaultOneShotAdapter
types = [DefaultOneShotAdapter] * 3
params = [dict(controller_type=GreedyOneShotAgent)] * 3
agents_per_process = [2, 1]
n_processes = len(agents_per_process)
n_lines = 10
common = dict(
n_lines=10,
shortfall_penalty_mean=0.2,
disposal_cost_mean=0.1,
shortfall_penalty_dev=0.01,
disposal_cost_dev=0.01,
storage_cost_mean=0.0,
storage_cost_dev=0.0,
)
# setup the factory profiles. For each factory we
profiles = [
OneShotProfile(cost=3, input_product=0, **common),
OneShotProfile(cost=10, input_product=0, **common),
OneShotProfile(cost=7, input_product=1, **common),
]
# create exogenous contracts
exogenous = [
## exogenous supply
OneShotExogenousContract(
product=0,
quantity=10,
unit_price=5,
time=1,
revelation_time=1,
seller=-1,
buyer=0,
),
OneShotExogenousContract(
product=0,
quantity=10,
unit_price=7,
time=2,
revelation_time=0,
seller=-1,
buyer=0,
),
## exogenous sales
OneShotExogenousContract(
product=0,
quantity=10,
unit_price=5,
time=1,
revelation_time=0,
seller=2,
buyer=-1,
),
]
world = SCML2023OneShotWorld(
catalog_prices=[10, 20, 30],
profiles=profiles,
agent_types=types,
agent_params=params,
exogenous_contracts=exogenous,
n_steps=5,
construct_graphs=True,
agent_name_reveals_position=True,
agent_name_reveals_type=True,
)
world.draw(what=["contracts-concluded"])
plt.show()
The world is constructed. Lets run it and see what happens:
.. code:: ipython3
world.run()
_, axs = plt.subplots(2)
world.draw(
what=["negotiations-started", "contracts-concluded"],
steps=(0, world.n_steps),
together=False,
axs=axs,
)
plt.show()
You can confirm for yourself that this is what we expected. Let’s dive
into the details.
Firstly, in this case, we need to pass ``agent_params`` to the
constructor (because ``OneshotAgent`` is a controller and not an
``Agent`` which means it needs an adapter to run. Here we use the
default ``DefaultOneshotAdapter``:
.. code:: python
types = [DefaultOneShotAdapter] * 3
params = [dict(controller_type=GreedyOneShotAgent)] * 3
The real agent type we want is to be passed in ``controller_type``.
The profile in this case has a different structure than the previous
case to match the `game
description `__. Other
than the production cost, we also need to pass the parameters of
Gaussians describing shortfall penalties and disposal costs.
Other than these two differences, the rest is almost the same as in the
previous case.
Download :download:`Notebook`.