{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "f80d6304", "metadata": { "execution": { "iopub.execute_input": "2022-02-19T02:34:16.788523Z", "iopub.status.busy": "2022-02-19T02:34:16.787558Z", "iopub.status.idle": "2022-02-19T02:34:16.794067Z", "shell.execute_reply": "2022-02-19T02:34:16.794496Z" }, "papermill": { "duration": 0.035523, "end_time": "2022-02-19T02:34:16.794730", "exception": false, "start_time": "2022-02-19T02:34:16.759207", "status": "completed" }, "tags": [ "remove_cell" ] }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%matplotlib inline\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "# setup disply parameters\n", "from matplotlib import pylab as plt\n", "from matplotlib.ticker import StrMethodFormatter\n", "float_formatter = StrMethodFormatter('{x:0.03f}')\n", "from IPython.core.display import display, HTML\n", "display(HTML(\"\"))\n", "SMALL_SIZE = 14\n", "MEDIUM_SIZE = 16\n", "BIGGER_SIZE = 20\n", "\n", "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n", "plt.rc('axes', titlesize=SMALL_SIZE) # fontsize of the axes title\n", "plt.rc('axes', labelsize=MEDIUM_SIZE) # fontsize of the x and y labels\n", "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n", "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n", "plt.rc('figure', figsize=(18, 6)) # set figure size\n", "plt.rc(\"animation\", html=\"html5\")" ] }, { "cell_type": "code", "execution_count": 2, "id": "0cf4c369", "metadata": { "execution": { "iopub.execute_input": "2022-02-19T02:34:16.834891Z", "iopub.status.busy": "2022-02-19T02:34:16.833467Z", "iopub.status.idle": "2022-02-19T02:34:17.372941Z", "shell.execute_reply": "2022-02-19T02:34:17.373605Z" }, "papermill": { "duration": 0.565418, "end_time": "2022-02-19T02:34:17.373877", "exception": false, "start_time": "2022-02-19T02:34:16.808459", "status": "completed" }, "tags": [ "remove_cell" ] }, "outputs": [], "source": [ "from abc import abstractmethod, ABC\n", "from collections import defaultdict\n", "from random import shuffle, random, sample, randint\n", "\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from negmas import (\n", " Action,\n", " Agent,\n", " NegotiatorMechanismInterface,\n", " AgentWorldInterface,\n", " Breach,\n", " Contract,\n", " Issue,\n", " make_issue,\n", " LinearUtilityFunction,\n", " MechanismState,\n", " Negotiator,\n", " RandomNegotiator,\n", " RenegotiationRequest,\n", " SAONegotiator,\n", " UtilityFunction,\n", " World,\n", ")\n", "from negmas.serialization import to_flat_dict\n", "from typing import Callable, List, Optional, Set, Dict, Any, Collection" ] }, { "cell_type": "code", "execution_count": 3, "id": "db77638d", "metadata": { "execution": { "iopub.execute_input": "2022-02-19T02:34:17.424085Z", "iopub.status.busy": "2022-02-19T02:34:17.422962Z", "iopub.status.idle": "2022-02-19T02:34:17.803779Z", "shell.execute_reply": "2022-02-19T02:34:17.804209Z" }, "papermill": { "duration": 0.41254, "end_time": "2022-02-19T02:34:17.804396", "exception": false, "start_time": "2022-02-19T02:34:17.391856", "status": "completed" }, "tags": [ "remove_cell" ] }, "outputs": [], "source": [ "from negmas import *" ] }, { "cell_type": "markdown", "id": "27a0f785", "metadata": { "papermill": { "duration": 0.016235, "end_time": "2022-02-19T02:34:17.831731", "exception": false, "start_time": "2022-02-19T02:34:17.815496", "status": "completed" }, "tags": [] }, "source": [ "## Develop a new simulation (world)\n", "\n", "A simulation is an embedded domain in which agents behave. It is represented in NegMAS by a `World`. This tutorial will take you through the process of developing a simple simulation (world).\n", "\n", "A `World` in NegMAS is not just a multi-agent world simulation. It was designed from the bottom up to simply the common tasks involved in constructing negotiation driven simulations. \n", "\n", "The simulation is divided into simulation steps (not to be confused with negotiation rounds). At every step, agents are allowed to act proactively by executing actions in the world, reading their state from the world, or requesting/running negotiations with other agents. \n", "\n", "The negotiation process follows the following steps:\n", "\n", "1. An agent requests a negotiation involving a set of agents (it may or may not be one of them) using either `request_negotiation` or `run_negotiation` methods (the later does not return until the negotiation is complete. The `World` can disable immediately running negotiations, or requesting negotiations that does not involve oneself, etc. The caller agent can also provide a `Negotiator` to represent it in this negotiation.\n", "2. All partners in the negotiation are asked to accept to start the negotiation through calls to `respond_to_negotiation_request`. The can accept by returning a `Negotiator` object to represent them or reject.\n", "3. Negotiations start depending on the rules given by the `World`. The default rule is to start negotiations only if ALL partners accepted the request but other rules like starting negotiations that are accepted by at least two partners are possible.\n", "4. The `Mechanism` objects are created by the `World` and negotiators supplied by all partners (that agreed to join the negotiation) are added to it. The `World` designer can fix the type of the negotiation mechanism to be used (e.g. `SAOMechanism`) or it can leave the choice to individual agents requesting negotiations.\n", "5. The negotiation is run and its result is recorded either as a failed negotiation (if no agreement is reached) or as a `Contract` if an agreement is reached.\n", "6. It is possible for world designers to either make `Contract`s binding upon agreement by having them automatically signed or to give agents one final chance to *sign* the contracts. Only signed contracts are considered binding.\n", "7. The `World` can also run a simulation that is affected by signed contracts. A common case is for contracts to have some *executing time* or a similar attribute that defines when do they become due.\n", "8. When a `Contract` becomes due, it is executed by the `World` as part of the simulation it is controlling. In some cases, the contract cannot be executed and this is considered a `Breach`.\n", "9. The world processes breaches and may decide to apply penalties to agents that caused them (if that makes sense for the specific simulation being conducted).\n", "10. Agents are informed about the final state of due contracts (i.e. whether they are executed successfully or breached). \n", "\n", "\n", "\n", "### Development Process\n", "\n", "To create a new world, you need to do the following steps:\n", "\n", "1. Create a new `AgentWorldInterface` for your world (that inherits from `AWI`) and provide easy to use methods that agents can use to access your world. A common functionality of the AWI is to confirm that negotiations make sense. We will see an example later in this tutorial\n", "2. Create a new `World` class that inherits from the base `World` and implement the required callback (your AWI from the first step will need to be registered in the world as will be shown later). \n", "3. Create a base `Agent` class for your world that inherits from `Agent` and provides the basic functionality shared by all agents in your world (if any)\n", "\n", "### The Trips World\n", "\n", "Let's consider a -- not so simple -- world. We have $m$ agents representing people agreeing about where to spend their vacation repeatedly. Every holiday season, an agent can request to negotiate with one or more other agents trying to arrange a trip. Each trip is described by the following attributes:\n", "\n", "* **Active**: Binary issue (either an active trip with lots of hiking, running ,etc or not)\n", "* **Duration**: Integer issue (between 1 and 7 days)\n", "* **Cost**: Continuous range issue (between 0.0 and 1.0)\n", "\n", "All agents involved in a negotiation must agree for the trip to be conducted. An agent can request at most $n$ negotiations in a single season. Each agent has some fixed but unknown probability of not honoring its agreement and failing to show up for the trip and only when all agents involved in a trip show up does the trip actually happen with each agent receiving its utility value as defined by its unchanging utility function.\n", "\n", "Let's implement this world in NegMAS." ] }, { "cell_type": "markdown", "id": "f673567e", "metadata": { "papermill": { "duration": 0.018262, "end_time": "2022-02-19T02:34:17.869848", "exception": false, "start_time": "2022-02-19T02:34:17.851586", "status": "completed" }, "tags": [] }, "source": [ "### Implementing the Agent-World-Interface\n", "\n", "We will start by defining the interface between the world and the agent as an AWI class" ] }, { "cell_type": "code", "execution_count": 4, "id": "accabbf4", "metadata": { "execution": { "iopub.execute_input": "2022-02-19T02:34:17.902614Z", "iopub.status.busy": "2022-02-19T02:34:17.901643Z", "iopub.status.idle": "2022-02-19T02:34:17.904253Z", "shell.execute_reply": "2022-02-19T02:34:17.903809Z" }, "papermill": { "duration": 0.023435, "end_time": "2022-02-19T02:34:17.904410", "exception": false, "start_time": "2022-02-19T02:34:17.880975", "status": "completed" }, "tags": [] }, "outputs": [], "source": [ "class AWI(AgentWorldInterface):\n", " @property\n", " def n_negs(self):\n", " \"\"\"Number of negotiations an agent can start in a step (holiday season)\"\"\"\n", " return self._world.neg_quota_step\n", "\n", " @property\n", " def agents(self):\n", " \"\"\"List of all other agent IDs\"\"\"\n", " return list(_ for _ in self._world.agents.keys() if _ != self.agent.id)\n", "\n", " def request_negotiation(\n", " self, partners: List[str], negotiator: SAONegotiator\n", " ) -> bool:\n", " \"\"\"A convenient way to request negotiations\"\"\"\n", " if self.agent.id not in partners:\n", " partners.append(self.agent.id)\n", " req_id = self.agent.create_negotiation_request(\n", " issues=self._world.ISSUES,\n", " partners=partners,\n", " negotiator=negotiator,\n", " annotation=dict(),\n", " extra=dict(negotiator_id=negotiator.id),\n", " )\n", " return self.request_negotiation_about(\n", " issues=self._world.ISSUES, partners=partners, req_id=req_id\n", " )" ] }, { "cell_type": "markdown", "id": "5cd840a5", "metadata": { "papermill": { "duration": 0.013735, "end_time": "2022-02-19T02:34:17.929560", "exception": false, "start_time": "2022-02-19T02:34:17.915825", "status": "completed" }, "tags": [] }, "source": [ "The minimum here is to define a way for agents to request negotiations form the world. The base `AgentWorldInterface` has a `request_negotiation_about` method that can be used for this purpose but it is too general and allows agents to set arbitrary issues and negotiation mechanisms. Usually you will want to restrict the types of negotiations allowed by defining a `request_negotiation` method which decides as much as possible for the agent.\n", "\n", "This is done here using the following method:\n", "\n", "```python\n", " def request_negotiation(\n", " self, partners: List[str], negotiator: SAONegotiator\n", " ) -> bool:\n", " ...\n", "```\n", "\n", "Here the agent is asked to provide only a list of `partners` and a `negotiator` to use. \n", "\n", "1. The AWI will then make sure that the agent is added to the partners list if it was not already in it:\n", "```python\n", " if self.agent.id not in partners:\n", " partners.append(self.agent.id)\n", "```\n", "\n", "2. A negotiation request is then created using `create_negotiation_request` of the agent connected to the AWI which is used to keep track of which requests are out there and which are accepted/rejected\n", "\n", "```python\n", " req_id = self.agent.create_negotiation_request(\n", " issues=self._world.ISSUES,\n", " partners=partners,\n", " negotiator=negotiator,\n", " annotation=dict(),\n", " extra=dict(negotiator_id=negotiator.id),\n", " )\n", "```\n", "\n", "3. Finally, the AWI requests the negotiation from the world using the base `request_negotiation_about` method.\n", "```python\n", " return self.request_negotiation_about(\n", " issues=self._world.ISSUES, partners=partners, req_id=req_id\n", " )\n", "```\n", "\n", "Other than this commonly provided method, the AWI provides two properties that can be accessed by the agent, `agents` which returns the IDs of all *other* agents in the world and `n_negs` which gives the total number of negotiations that the agent can *start* in a single step (holiday season)." ] }, { "cell_type": "markdown", "id": "f9fa9d28", "metadata": { "papermill": { "duration": 0.014706, "end_time": "2022-02-19T02:34:17.959091", "exception": false, "start_time": "2022-02-19T02:34:17.944385", "status": "completed" }, "tags": [] }, "source": [ "#### Implement the base world class\n", "\n", "To implement the trips world, you will need to override the abstract methods of `World`. You will usually need also to override `__init__` to initialize your agent and `join` to set up any agent specific information you need to keep. Here is the full implementation:" ] }, { "cell_type": "code", "execution_count": 5, "id": "bd4074c9", "metadata": { "execution": { "iopub.execute_input": "2022-02-19T02:34:18.001890Z", "iopub.status.busy": "2022-02-19T02:34:17.985671Z", "iopub.status.idle": "2022-02-19T02:34:18.004733Z", "shell.execute_reply": "2022-02-19T02:34:18.005154Z" }, "papermill": { "duration": 0.034231, "end_time": "2022-02-19T02:34:18.005331", "exception": false, "start_time": "2022-02-19T02:34:17.971100", "status": "completed" }, "tags": [] }, "outputs": [], "source": [ "class TripsWorld(World):\n", " ISSUES = [\n", " make_issue((0.0, 1.0), \"cost\"),\n", " make_issue(2, \"active\"),\n", " make_issue((1, 7), \"duration\"),\n", " ]\n", "\n", " def __init__(self, *args, **kwargs):\n", " \"\"\"Initialize the world\"\"\"\n", " kwargs[\"awi_type\"] = AWI\n", " kwargs[\"negotiation_quota_per_step\"] = kwargs.get(\n", " \"negotiation_quota_per_step\", 8\n", " )\n", " kwargs[\"force_signing\"] = True\n", " kwargs[\"default_signing_delay\"] = 0\n", " super().__init__(*args, **kwargs)\n", " self._contracts: Dict[int, List[Contract]] = defaultdict(list)\n", " self._total_utility: Dict[str, float] = defaultdict(float)\n", " self._ufuns: Dict[str, UtilityFunction] = dict()\n", " self._breach_prob: Dict[str, float] = dict()\n", "\n", " def join(\n", " self,\n", " x: Agent,\n", " preferences: Preferences | None = None,\n", " breach_prob: float | None = None,\n", " **kwargs,\n", " ):\n", " \"\"\"Define the ufun and breach-probability for each agent\"\"\"\n", " super().join(x, **kwargs)\n", " weights = np.random.rand(len(self.ISSUES)) - 0.5\n", " x.ufun = (\n", " LinearUtilityFunction(weights, reserved_value=0.0) if ufun is None else ufun\n", " )\n", " self._ufuns[x.id] = x.ufun\n", " self._breach_prob[x.id] = random() * 0.1 if breach_prob is None else breach_prob\n", "\n", " def simulation_step(self, stage: int = 0):\n", " \"\"\"What happens in this world? Nothing\"\"\"\n", " pass\n", "\n", " def get_private_state(self, agent: Agent) -> dict:\n", " \"\"\"What is the information available to agents? total utility points\"\"\"\n", " return dict(total_utility=self._total_utility[agent.id])\n", "\n", " def execute_action(\n", " self, action: Action, agent: Agent, callback: Callable | None = None\n", " ) -> bool:\n", " \"\"\"Executing actions by agents? No actions available\"\"\"\n", " pass\n", "\n", " def on_contract_signed(self, contract: Contract) -> None:\n", " \"\"\"Save the contract to be executed in the following hoiday season (step)\"\"\"\n", " super().on_contract_signed(contract)\n", " self._contracts[self.current_step + 1].append(contract)\n", "\n", " def executable_contracts(self):\n", " \"\"\"What contracts are to be executed in the current step?\n", " Ones that were signed the previous step\"\"\"\n", " return self._contracts[self.current_step]\n", "\n", " def order_contracts_for_execution(\n", " self, contracts: Collection[Contract]\n", " ) -> Collection[Contract]:\n", " \"\"\"What should be the order of contract execution? Random\"\"\"\n", " shuffle(contracts)\n", " return contracts\n", "\n", " def start_contract_execution(self, contract: Contract) -> Optional[Set[Breach]]:\n", " \"\"\"What should happen when a contract comes due?\n", " 1. Find out if it will be breached\n", " 2. If not, add to each agent its utility from the trip\n", " \"\"\"\n", " breaches = []\n", " for aid in contract.partners:\n", " if random() < self._breach_prob[aid]:\n", " breaches.append(\n", " Breach(\n", " contract,\n", " aid,\n", " \"breach\",\n", " victims=[_ for _ in contract.partners if _ != aid],\n", " )\n", " )\n", " if len(breaches) > 0:\n", " return set(breaches)\n", " for aid in contract.partners:\n", " self._total_utility[aid] += self._ufuns[aid](contract.agreement)\n", " return set()\n", "\n", " def complete_contract_execution(\n", " self, contract: Contract, breaches: List[Breach], resolution: Contract\n", " ) -> None:\n", " \"\"\"What happens if a breach was resolved? Nothing. They cannot\"\"\"\n", " pass\n", "\n", " def delete_executed_contracts(self) -> None:\n", " \"\"\"Removes all contracts for the current step\"\"\"\n", " if self._current_step in self._contracts.keys():\n", " del self._contracts[self.current_step]\n", "\n", " def contract_record(self, contract: Contract) -> Dict[str, Any]:\n", " \"\"\"Convert the contract into a dictionary for saving\"\"\"\n", " return to_flat_dict(contract)\n", "\n", " def breach_record(self, breach: Breach) -> Dict[str, Any]:\n", " \"\"\"Convert the breach into a dictionary for saving\"\"\"\n", " return to_flat_dict(breach)\n", "\n", " def contract_size(self, contract: Contract) -> float:\n", " \"\"\"How good is a contract? Welfare\"\"\"\n", " if contract.agreement is None:\n", " return 0.0\n", " return sum(self._ufuns[aid](contract.agreement) for aid in contract.partners)\n", "\n", " def post_step_stats(self):\n", " for aid, agent in self.agents.items():\n", " self._stats[f\"total_utility_{agent.name}\"].append(self._total_utility[aid])" ] }, { "cell_type": "markdown", "id": "459a615a", "metadata": { "papermill": { "duration": 0.012635, "end_time": "2022-02-19T02:34:18.029949", "exception": false, "start_time": "2022-02-19T02:34:18.017314", "status": "completed" }, "tags": [] }, "source": [ "We will now inspect each of these methods in turn.\n", "\n", "#### Constructing the world\n", "\n", "The first thing to do when constructing the world in `__init__` is to call the `World` class constructor forcing some of the parameters. This is done here:\n", "```python\n", " kwargs[\"awi_type\"] = AWI\n", " kwargs[\"negotiation_quota_per_step\"] = kwargs.get(\n", " \"negotiation_quota_per_step\", 8\n", " )\n", " kwargs[\"force_signing\"] = True\n", " kwargs[\"default_signing_delay\"] = 0 \n", " super().__init__(*args, **kwargs)\n", "```\n", "\n", "Of note is setting the `awi_type` to the `AWI` class we have just created. This allows agents to access members of this class through their `awi` property as we will see later.\n", "\n", "Moreover, we force the `negotiation_quota_per_step` to be no more than 8 (the default is $\\inf$) and force signing of all contracts which will make contracts binding immediately once agreements are reached through negotiation.\n", "\n", "We then define four data-members that we keep track of:\n", "\n", "```python\n", " self._contracts: Dict[int, List[Contract]] = defaultdict(list)\n", " self._total_utility: Dict[str, float] = defaultdict(float)\n", " self._ufuns: Dict[str, UtilityFunction] = dict()\n", " self._breach_prob: Dict[str, float] = dict() \n", "```\n", "* **_contracts** maps step number to the contracts to be executed in it.\n", "* **_total_utility, _ufuns, _breach_prob** maps agent ID to the total utility it currently has, its utility function and its breach probability (the probability of not showing up for a trip).\n", "\n", "\n", "#### Joining the world\n", "\n", "Agents join the world by calls to the `join` method. \n", "* The first thing to do is to call the base `join` method of the `World` class. That is **essential** for the system to work properly. Whenever you override a method that is not marked abstract, you **must** call the base class version using `super()`:\n", "\n", "```python\n", " def join(self, x: Agent, preferences: Preferences | None = None, breach_prob: float | None = None, **kwargs):\n", " \"\"\"Define the ufun and breach-probability for each agent\"\"\"\n", " super().join(x, **kwargs)\n", " ...\n", "```\n", "\n", "* We need to override the this method in order to set the utility function of the agent that just joined and its breach probability. In both cases, we use the value provided by the user if any and generate an appropriate random value if nothing is provided. \n", "\n", "```python\n", " x.ufun = LinearUtilityFunction(\n", " np.random.rand(len(self.ISSUES)) - 0.5\n", " ) if ufun is None else ufun\n", " self._ufuns[x.id] = x.ufun\n", " self._breach_prob[x.id] = random() * 0.1 if breach_prob is None else breach_prob\n", "```\n", "\n", "#### Simulation, action, and state\n", "\n", "The `TripsWorld` does not have a simulation. Nothing really happens in this world. This means we can just do nothing in the `simulation_step` method\n", "```python\n", " def simulation_step(self, stage: int = 0):\n", " \"\"\"What happens in this world? Nothing\"\"\"\n", " pass\n", "```\n", "\n", "Every world needs to define what is the private state of an agent (available to it through `self.awi.state`). In our world, the private state of an agent is the total utility it collected so far.\n", "\n", "```python\n", " def get_private_state(self, agent: Agent) -> dict:\n", " \"\"\"What is the information available to agents? total utility points\"\"\"\n", " return dict(total_utility=self._total_utility[agent.id])\n", "```\n", "\n", "As we have no actual simulation, there are not actions that the agent can execute in the world, so `execute_action` does nothing.\n", "```python\n", " def execute_action(\n", " self, action: Action, agent: Agent, callback: Callable | None = None\n", " ) -> bool:\n", " \"\"\"Executing actions by agents? No actions available\"\"\"\n", " pass\n", "```\n", "\n", "#### Contract Management\n", "\n", "The `TripsWorld` is responsible of managing contracts. The base `World` class will take care of most of the process but it needs the `TripsWorld` to respond to some callbacks in order to manage contract execution, storage, and optionally renegotiations of breached contracts.\n", "\n", "The callbacks related to this are:\n", "```python\n", " def on_contract_signed(self, contract: Contract) -> None:\n", " ...\n", " def executable_contracts(self) -> Collection[Contract]:\n", " ...\n", " def order_contracts_for_execution( self, contracts: Collection[Contract]) -> Collection[Contract]:\n", " ...\n", " def start_contract_execution(self, contract: Contract) -> Optional[Set[Breach]]:\n", " ...\n", " def complete_contract_execution( self, contract: Contract, breaches: List[Breach]\n", " , resolution: Contract) -> None:\n", " ...\n", " def delete_executed_contracts(self) -> None:\n", " ...\n", " def contract_record(self, contract: Contract) -> Dict[str, Any]:\n", " ...\n", " def breach_record(self, breach: Breach) -> Dict[str, Any]:\n", " ...\n", " def contract_size(self, contract: Contract) -> float:\n", " ...\n", "```\n", "The names are almost self-explanatory and we will go through them one by one:\n", "\n", "1. **on_contract_signed** This method is called whenever a contract is signed (becomes binding). This is the only non-abstract method in the contract related set here but we need to override it in order to keep track of the execution time of the contract using:\n", "```python\n", " self._contracts[self.current_step + 1].append(contract)\n", "```\n", "2. **executable_contracts** Should return a list of contracts that are due at the current step. We simply use the `_contracts` mapping we updated in `on_contract_signed`:\n", "```python\n", " return self._contracts[self.current_step]\n", "```\n", "3. **order_contracts_for_execution** The world is responsible of deciding that order at which contracts (returned from `executable_contracts`) are executed, here we just shuffle them randomly and return them:\n", "```python\n", " shuffle(contracts)\n", " return contracts\n", "```\n", "4. **start_contract_execution** Here we decide how to execute a contract. We first check whether each agent of the partners who signed the contract is going to show up using its breach-probability and record a breach for each such event. If there are any breaches, we stop processing because the trip did not take place:\n", "```python\n", " breaches = []\n", " for aid in contract.partners:\n", " if random() < self._breach_prob[aid]:\n", " breaches.append(\n", " Breach(\n", " contract, aid, \"breach\",\n", " victims=[_ for _ in contract.partners if _ != aid],\n", " )\n", " )\n", " if len(breaches) > 0:\n", " return set(breaches)\n", "```\n", "If there are no breaches, the trip is assumed to execute successfully and every agent (of the partners) is assigned the utility value from that trip according to its utility function:\n", "```python\n", " for aid in contract.partners:\n", " self._total_utility[aid] = self._ufuns[aid](contract.agreement)\n", " return set()\n", "```\n", "5. **complete_contract_execution** This method is only called in worlds that allow re-negotiation of breaches. As our world does not have the concept, we just do nothing here.\n", "6. **delete_executed_contracts** This method is responsible of cleaning up all contracts that have been processed in the current step. We again use the mapping we constructed in `__init__` and updated in `on_contract_signed`.\n", "```python\n", " if self._current_step in self._contracts.keys():\n", " del self._contracts[self.current_step]\n", "```\n", "\n", "\n", "These six steps complete all processing of contracts. Nevertheless, we still need to override three other methods to define how contracts and breaches are stored and the *value* of a contract\n", "\n", "* **contract_record** should return a dictionary representing a contract. We simply convert it to a dictionary using a helper function from negmas `to_flat_dict`:\n", "```python\n", " return to_flat_dict(contract)\n", "```\n", "* **breach_record** Same as contract record but for breaches. We do the same.\n", "```python\n", " return to_flat_dict(breach)\n", "```\n", "* **contract_size** Used to specify some sense of size for contracts. This is only used for statistics and does not affect the operation of the simulation. We define the contract size here as the welfar (total utility of all partners):\n", "```python\n", " if contract.agreement is None:\n", " return 0.0\n", " return sum(self._ufuns[aid](contract.agreement) for aid in contract.partners)\n", "```\n", "\n", "This complete the world and agent-world-interface design. We can now develop our base agent class.\n", "\n", "#### Statisitcs\n", "\n", "The base `World` keeps track of negotiation related statistics (e.g. how many negotiations were requested very step, how many contracted were breached, etc). You can easily add to this set of statistics by overloading `post_step_stats` (and the corresponding `pre_step_stats` if needed). In our world, we just add one custom statistic: the total utility collected by the agent so far:\n", "```python\n", " for aid, agent in self.agents.items():\n", " self._stats[f\"total_utility_{agent.name}\"].append(self._total_utility[aid])\n", "```\n", "\n", "Note that we used the agent name not ID to differentiate these statistics. Because the system does not know or use our statistic, we can use the name which will usually be easier to read when inspecting these statistics as we will see in the following tutorial\n" ] }, { "cell_type": "markdown", "id": "1d175769", "metadata": { "papermill": { "duration": 0.014091, "end_time": "2022-02-19T02:34:18.059652", "exception": false, "start_time": "2022-02-19T02:34:18.045561", "status": "completed" }, "tags": [] }, "source": [ "### Base Agent (Person)\n", "\n", "Even though it is not strictly necessary (as with the case of agent-world-interface), it is useful to provide a base agent that hides unnecessary details from developers of agents targeting our `TripsWorld`. This is the complete listing of our base agent:" ] }, { "cell_type": "code", "execution_count": 6, "id": "7d1e8cb0", "metadata": { "execution": { "iopub.execute_input": "2022-02-19T02:34:18.095326Z", "iopub.status.busy": "2022-02-19T02:34:18.094427Z", "iopub.status.idle": "2022-02-19T02:34:18.096598Z", "shell.execute_reply": "2022-02-19T02:34:18.097023Z" }, "papermill": { "duration": 0.024338, "end_time": "2022-02-19T02:34:18.097200", "exception": false, "start_time": "2022-02-19T02:34:18.072862", "status": "completed" }, "tags": [] }, "outputs": [], "source": [ "class Person(Agent, ABC):\n", " @abstractmethod\n", " def step(self):\n", " ...\n", "\n", " @abstractmethod\n", " def init(self):\n", " ...\n", "\n", " @abstractmethod\n", " def respond_to_negotiation_request(\n", " self,\n", " initiator: str,\n", " partners: List[str],\n", " mechanism: NegotiatorMechanismInterface,\n", " ) -> Optional[Negotiator]:\n", " ...\n", "\n", " def _respond_to_negotiation_request(\n", " self,\n", " initiator: str,\n", " partners: List[str],\n", " issues: List[Issue],\n", " annotation: Dict[str, Any],\n", " mechanism: NegotiatorMechanismInterface,\n", " role: Optional[str],\n", " req_id: Optional[str],\n", " ) -> Optional[Negotiator]:\n", " return self.respond_to_negotiation_request(initiator, partners, mechanism)\n", "\n", " def on_neg_request_rejected(self, req_id: str, by: Optional[List[str]]):\n", " pass\n", "\n", " def on_neg_request_accepted(\n", " self, req_id: str, mechanism: NegotiatorMechanismInterface\n", " ):\n", " pass\n", "\n", " def on_negotiation_failure(\n", " self,\n", " partners: List[str],\n", " annotation: Dict[str, Any],\n", " mechanism: NegotiatorMechanismInterface,\n", " state: MechanismState,\n", " ) -> None:\n", " pass\n", "\n", " def on_negotiation_success(\n", " self, contract: Contract, mechanism: NegotiatorMechanismInterface\n", " ) -> None:\n", " pass\n", "\n", " def set_renegotiation_agenda(\n", " self, contract: Contract, breaches: List[Breach]\n", " ) -> Optional[RenegotiationRequest]:\n", " pass\n", "\n", " def respond_to_renegotiation_request(\n", " self, contract: Contract, breaches: List[Breach], agenda: RenegotiationRequest\n", " ) -> Optional[Negotiator]:\n", " pass\n", "\n", " def on_contract_executed(self, contract: Contract) -> None:\n", " pass\n", "\n", " def on_contract_breached(\n", " self, contract: Contract, breaches: List[Breach], resolution: Optional[Contract]\n", " ) -> None:\n", " pass\n" ] }, { "cell_type": "markdown", "id": "cbd2766e", "metadata": { "papermill": { "duration": 0.011909, "end_time": "2022-02-19T02:34:18.121741", "exception": false, "start_time": "2022-02-19T02:34:18.109832", "status": "completed" }, "tags": [] }, "source": [ "The first thing, our abstract-base-class (ABC) does is defining the abstract methods that must be implemented by any agent that is compatible with the `TripsWorld`. \n", "\n", "The first two abstract methods are `init` and `step` called by the world to initialize the agent (after its AWI is created) and at every simulation step. These methods are not abstract in the base `Agent` class but we convert them to abstract methods to force all `Person` based agents to provide some implementation for them\n", "\n", "```python\n", " @abstractmethod\n", " def step(self):\n", " ...\n", "\n", " @abstractmethod\n", " def init(self):\n", " ...\n", "```\n", "\n", "We then add a third method for responding to negotiation requests:\n", "```python\n", " @abstractmethod\n", " def respond_to_negotiation_request(\n", " self, initiator: str, partners: List[str], mechanism: AgentMechanismInterface,\n", " ) -> Optional[Negotiator]:\n", " ...\n", "```\n", "\n", "`World` and `TripWorld` classes know nothing about this method, our base `Person` class will call it when it receives a request to respond to a negotiation request from the world in `_respond_to_negotiation_request` (notice the underscore which indicates that children should not modify this method):\n", "```python\n", " return self.respond_to_negotiation_request(initiator, partners, mechanism)\n", "```\n", "This arrangement removes the need to pass several parameters of `_respond_to_negotiation_request` that are not of value for our current simulation.\n", "\n", "We provide a do-nothing implementation of all other callbacks expected during the simulation. These are:\n", "\n", "```python\n", " def on_neg_request_rejected(self, req_id: str, by: Optional[List[str]]):\n", " ...\n", " def on_neg_request_accepted(self, req_id: str, mechanism: AgentMechanismInterface):\n", " ...\n", " def on_negotiation_failure( self, partners: List[str], annotation: Dict[str, Any],\n", " mechanism: AgentMechanismInterface, state: MechanismState,):\n", " ...\n", " def on_negotiation_success( self, contract: Contract, mechanism: AgentMechanismInterface):\n", " ...\n", " def set_renegotiation_agenda(\n", " self, contract: Contract, breaches: List[Breach]) -> Optional[RenegotiationRequest]:\n", " ...\n", " def respond_to_renegotiation_request(\n", " self, contract: Contract, breaches: List[Breach], agenda: RenegotiationRequest) -> Optional[Negotiator]:\n", " ...\n", " def on_contract_executed(self, contract: Contract):\n", " ...\n", " def on_contract_breached(self, contract: Contract, breaches: List[Breach]):\n", " ...\n", "```\n", "\n", "These callbacks are called by the world at key points of the process from a negotiation request to an exeucted/breached contract. The names are self-explanatory but we summarize them here:\n", "\n", "* **on_neg_request_rejectect/accepted** Called to tell the agent about the fate of a negotiation request it initiated (using `request_negotiation`). Agents can access the current requests using their `negotiation_requests` property to get more information about the request if needed.\n", "* **on_negotiation_failure.success** Called to tell the agent about the fate of negotiations it engaged in (using its own negotiators). The agent can access the current set of negotiations using its `negotiations` property.\n", "* **set_renegotiation_agenda/respond_to_renegotiation_request** Only needed for worlds that allow re-negotiation of breached contracts.\n", "* **on_contract_executed/breached** Called to tell the agent about the fate of a contract it signed. \n", "* There is also an **on_contracts_finalized** callback that is used to tell the agent about which of its agreements have been signed by everyone and became binding and which were canceled because one or more of the partners refused to sign it. In our world, singing is forced so this callback is not necessary and it is not abstract so we did not implement it.\n", "\n", "We now have all the ingredients to create specific agents and start simulations. In the next tutorial we will develop an agent for this world and use it to test it." ] }, { "cell_type": "markdown", "id": "0960e20a", "metadata": { "papermill": { "duration": 0.012228, "end_time": "2022-02-19T02:34:18.145606", "exception": false, "start_time": "2022-02-19T02:34:18.133378", "status": "completed" }, "tags": [] }, "source": [ "### [ADVANCED] Most Important Functionality Provided by `World`\n", "\n", "This section is more of a reference. You need not go through it in details in your first read\n", "\n", "You can control several options about how your world simulation runs by setting constructor parameters of the `World` class (as we did earlier with `force_signing`). Here we discuss briefly some of the most important options. \n", "\n", "#### General \n", "These are general parameters that do not directly affect how the world works. The most important of these are `name` to set a name for the world, and `awi_type` which controls the type of AWI used to connect agents to it.\n", "\n", "* name: World Name\n", "* bulletin_board: A bulletin board object to use. If not given one will be created\n", "* awi_type: The type used for agent world interfaces (must descend from or behave like `AgentWorldInterface` )\n", "* info: A dictionary of key-value pairs that is kept within the world but never used. It is useful for storing contextual information. For example, when running tournaments.\n", "\n", "#### Simulation parameters\n", "These options control how the simulation is run and the order of operations in each simulation step.\n", "\n", "* n_steps: Total simulation time in steps\n", "* time_limit: Real-time limit on the simulation\n", "* operations: A list of `Operations` to run in order during every simulation step. You can use this parameter to set the order of events in your simulation. For example, you can choose when negotiations run relative to the `simulation_step` of your world.\n", " Available operations include:\n", " \n", " * StatsUpdate: Updating statistics stored in `_stats` by calling `update_stats` of the `World` class. Each time this operation is conducted a higher `stage` is passed to the `update_stats` method (the first such call will by default run `pre_step_stats` and later calls will call `post_step_stats` but you can change that.\n", " * SimulationStep: Calling `simulation_step` of the `World` class. Each time this operation is conducted a higher `stage` is passed to `simulation_step`.\n", " * Negotiations: Running all registered negotiations\n", " * ContractSigning: Sign contracts (if enabled)\n", " * ContractExecution: Execute contracts\n", " * AgentSteps: Step all agents by calling their `step` method\n", "\n", "\n", "#### Negotiation Parameters\n", "\n", "Controls all negotiations conducted during the simulation. \n", "\n", "* negotiation_speed: The number of negotiation steps per simulation step. None means infinite\n", "* neg_n_steps: Maximum number of steps allowed for a negotiation.\n", "* neg_step_time_limit: Time limit for single step of the negotiation protocol.\n", "* neg_time_limit: Real-time limit on each single negotiation\n", "* negotiation_quota_per_step: Number of negotiations an agent is allowed to start per step\n", "* negotiation_quota_per_simulation: Number of negotiations an agent is allowed to start in the simulation\n", "* start_negotiations_immediately: If true negotiations start immediately when registered rather than waiting for the next step\n", "* mechanisms: The mechanism types allowed in this world associated with each keyword arguments to be passed to it. This is a dictionary mapping a class name defining a mechanism to the parameters to use by default for that mechanism. For example, to allow both stacked alternating offers (with some custom setting) and the veto single text mechanisms in your world, you can use something like:\n", "```python\n", " super().__init__(mechanisms={\"negmas.sao.SAOMechanism\": dict(offering_is_accepting=False), \"negmas.st.STVetoMechanism\": dict()},\n", " ...)\n", "```\n", "\n", "#### Signing parameters\n", "\n", "After negotiations are concluded with agreements, it is possible to have an extra signing step to confirm these agreements before they become binding contracts. This gives agents central control over the agreements reached by their negotiators. You can control whether or not this step is needed for any world simulation and how confirmation (i.e. signing) is done through these parameters.\n", "\n", "* default_signing_delay: The default number of steps between contract conclusion and signing it. Only takes effect if `force_signing` is `False`\n", "* force_signing: If true, agents are not asked to sign contracts. They are forced to do so. In this case, `default_singing_delay` is not effective and signature is immediate\n", "* batch_signing: If true, contracts are signed in batches not individually\n", "\n", "\n", "#### Breach Processing\n", "\n", "When contracts fail to execute, breaches occur. You can control what happens when breaches occur using this parameter.\n", "\n", "* breach_processing: How to handle breaches. Can be any of `BreachProcessing` values. Three options are available:\n", " * No processing. In this case, the breach is just reported to the breach-list on the bulletin board and that is it.\n", " * Victim, then perpetrator. In this case victims of the breach are given the chance to propose a resolution followed by the perpetrator.\n", " * Renegotiation, where a complete negotiation session is conducted to resolve the breach.\n", "\n", "#### Logging\n", "\n", "NegMAS supports both general logs through the `log*` methods of the `World` class and agent specific logs through the `agent_log*` methods of the AWI. These parameters control logging. The default logging location is `~/negmas/logs`.\n", "\n", "* log_folder: Folder to save all logs\n", "* log_to_file: If true, will log to a file\n", "* log_file_name: Name of the log file\n", "* log_file_level: The log-level to save to file (WARNING, ERROR, INFO, DEBUG, CRITICAL, ...)\n", "* log_ufuns: Log utility functions\n", "* log_negotiations: Log all negotiation events\n", "* log_to_screen: Whether to log to screen\n", "* log_screen_level: The log-level to show on screen (WARNING, ERROR, INFO, DEBUG, CRITICAL, ...)\n", "* no_logs: If True, All logging will be disabled no matter what other options are given.\n", "* log_stats_every: If nonzero and positive, the period of saving stats\n", "* construct_graphs: If true, information needed to draw graphs using `draw` method are kept.\n", "\n", "#### What to save\n", "\n", "These settings greatly affect the memory consumption of the simulation. It tells NegMAS what exactly do you need to save in-memory.\n", "\n", "\n", "* save_signed_contracts: Save all signed contracts\n", "* save_cancelled_contracts: Save all canceled contracts\n", "* save_negotiations: Save all negotiation records\n", "* save_resolved_breaches: Save all resolved breaches\n", "* save_unresolved_breaches: Save all unresolved breaches\n", "\n", "#### Exception Handling\n", "\n", "It is inevitable that exceptions will happen in agent code or the simulation. This set of parameters control how to handle these exceptions.\n", "\n", "* ignore_agent_exceptions: Ignore agent exceptions and keep running\n", "* ignore_mechanism_exceptions: If true, all mechanism exceptions are ignored and the mechanism is aborted\n", "* ignore_simulation_exceptions: Ignore simulation exceptions and keep running\n", "* ignore_contract_execution_exceptions: Ignore contract execution exceptions and keep running\n", "* safe_stats_monitoring: Never throw an exception for a failure to save stats or because of a Stats Monitor object\n", "\n", "#### Checkpoints\n", "\n", "NegMAS can keep checkpoints of the world simulation that can be used to recover and continue the simulation later. These checkpoints are not stored by default but you can enable them and control their frequency and location using this set of parameters\n", "\n", "* checkpoint_every: The number of steps to checkpoint after. Set to <= 0 to disable\n", "* checkpoint_folder: The folder to save checkpoints into. Set to None to disable\n", "* checkpoint_filename: The base filename to use for checkpoints (multiple checkpoints will be prefixed with step number).\n", "* single_checkpoint: If true, only the most recent checkpoint will be saved.\n", "* extra_checkpoint_info: Any extra information to save with the checkpoint in the corresponding json file as\n", " a dictionary with string keys\n", "* exist_ok: IF true, checkpoints override existing checkpoints with the same filename.\n" ] }, { "cell_type": "markdown", "id": "9daeef7f", "metadata": { "papermill": { "duration": 0.013203, "end_time": "2022-02-19T02:34:18.174122", "exception": false, "start_time": "2022-02-19T02:34:18.160919", "status": "completed" }, "tags": [] }, "source": [ "We can now continue to the next tutorials in which we will develop agents for your newly created world." ] }, { "cell_type": "code", "execution_count": null, "id": "2084e2e8", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "negmas", "language": "python", "name": "negmas" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.4" }, "papermill": { "default_parameters": {}, "duration": 5.417007, "end_time": "2022-02-19T02:34:18.609748", "environment_variables": {}, "exception": null, "input_path": "/Users/yasser/code/projects/negmas/notebooks/tutorials/04.develop_new_simulation.ipynb", "output_path": "/Users/yasser/code/projects/negmas/notebooks/tutorials/04.develop_new_simulation.ipynb", "parameters": {}, "start_time": "2022-02-19T02:34:13.192741", "version": "2.3.4" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 5 }