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`.