{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Building a Model in Helipad" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this walkthrough, we’ll build a very simple model of two goods where decentralized trading results in agents converging on an equilibrium price. Each period, agents pair off randomly and see if they can become better off by trading. If so, they trade. If not, they do nothing. In just a few rounds, agents – without knowing anything besides the people they’re trading with – converge on a single price for one good in terms of the other, and become better off in the process. You can see the [final model code here](https://github.com/charwick/helipad/blob/master/sample-models/pricediscover.py).\n", "\n", "In order to get started, we’ll need to import the main Helipad class and initialize it, along with a few other functions we’ll need along the way." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from helipad import Helipad\n", "from helipad.utility import CobbDouglas\n", "from math import exp, floor\n", "import random\n", "\n", "heli = Helipad()\n", "heli.name = 'Price Discovery'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The variable `heli` will be the main way we’ll interact with the model. Helipad operates using [*hooks*](https://helipad.dev/glossary/hooks/), meaning that Helipad runs a loop and gives you the opportunity to insert your own logic into it. In this model, we’ll insert code to run in three places: (1) at the beginning of each period, (2) when an agent is created, and – most importantly – (3) each time an agent is activated. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Outlining the Model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we’ll want to lay out the logic of the model, as well as the kinds of data we’ll want to collect as it runs. A rough outline might look like this:\n", "\n", "1. At the beginning of the model, each agent gets a random amount of two goods. We’ll call them Shmoo (M) and Soma (H).\n", "2. Agents have a Cobb-Douglas utility function over the two: *U*=*M*0.5*H*0.5. This means that more of each makes agents better off, but at a decreasing rate.\n", "3. Each period, each agent finds a random partner.\n", "4. If the agents in a pair find they can both become better off by trading, they trade some amount of Soma for some amount of Shmoo. Otherwise they do nothing.\n", "5. At the end of each period, we’ll be interested in recording (1) how well off everyone is, (2) how much shmoo and soma got traded, and (3) the terms of trade – specifically, how divergent they are.\n", "\n", "Helipad will take care of (3) for us:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "heli.agents.order = 'match'" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "This line configures the model to match agents each period and run them through a [`match`](https://helipad.dev/hooks/match) hook that we'll define later, rather than stepping through them individually. `'match'` is equivalent to `'match-2'`. We could also set [`heli.agents.order`](https://helipad.dev/functions/agents#order) to `'match-3'` if we wanted it to group in triplets, or any other number. But for a trading model, and for most others, pairs are what we want.\n", "\n", "For the others, we’ll hook (1) and (2) into [agent initialization](https://helipad.dev/hooks/agentinit), (4) into [agent activation](https://helipad.dev/hooks/agentstep), and (5) we’ll gather afterward.\n", "\n", "Starting with (1), we might want to control the aggregate ratio of shmoo to soma before each run. Helipad has a control panel on which we can place parameters that control the model. Before the function then, we’ll tell the Helipad object to add a parameter that we can control, and whose value we can use in our agents’ logic." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "heli.params.add('ratio', 'Log Endowment Ratio', 'slider', dflt=0, opts={'low': -3, 'high': 3, 'step': 0.5}, runtime=False)\n", "\n", "#Make sure we don't get stray agents\n", "heli.params['num_agent'].opts['step'] = 2\n", "heli.params['num_agent'].opts['low'] = 2\n", "\n", "heli.goods.add('shmoo','#11CC00', (1, 1000))\n", "heli.goods.add('soma', '#CC0000', lambda breed: (1, floor(exp(heli.param('ratio'))*1000)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Line 1 tells Helipad to add a slider parameter named ‘ratio’, with a default value of 0, and that moves in increments of 0.5 between -3 and 3. We'll be able to adjust this value in the control panel and use the value in the model. The `runtime` argument tells Helipad not to allow the parameter to be changed while the model is running, since it only affects the endowment at the beginning.\n", "\n", "Helipad automatically creates slider parameters for number of agents (more specifically, it creates a parameter for each *primitive*, but we aren't creating new primitives in this model, so we use the default primitive titled `agent`). In the fourth and fifth lines, we want to edit the options of this automatically created parameter so we can only create an even number of agents. Since this is a matching model, we don't want leftover agents! \n", "\n", "We can access this automatically created parameter in Helipad's `params` property. The [`opts` property](https://helipad.dev/functions/param/#opts) of the [`Param` object](https://helipad.dev/functions/param/) that we access this way corresponds to the `opts` argument in the [`Params.add()` method](https://helipad.dev/functions/params/add/) earlier. Here, we change the increment and the low value to equal 2.\n", "\n", "Lines 7 and 8 tell Helipad that our economy has two goods, named `'shmoo'` and `'soma'`. The [`Goods.add`](https://helipad.dev/functions/goods/add) function gives each agent object a [`'stocks`' property](https://helipad.dev/functions/agent#stocks), a dict with two items – `'soma'` and `'shmoo'` – that keep track of how much of each good each agent has. The second argument is a [hex color](https://www.w3schools.com/colors/colors_picker.asp); this tells Helipad to draw shmoo with a green line, and soma with a red.\n", "\n", "The third argument gives each agent an initial endowment. There are several ways we can do this. First, we could pass a number to give each agent the same amount. Second, we can pass a tuple with two items, and Helipad will endow the agent with a random amount between those two numbers. This is what we do for shmoo. For soma, on the other hand, we use the third possibility: a function that uses logic as complicated as necessary to determine how much each agent is to start with. In this case we want to endow the agent with a random amount between 0 and $1000 \\times e^{ratio}$, with `heli.param('ratio')` retrieving the value of the slider parameter from line 1 (The value is wrapped in `floor()` in order to make sure we pass a whole number to `randint()`). `'ratio'`, therefore, controls the aggregate quantity of soma as compared to shmoo.\n", "\n", "Why not just pass a tuple for soma too? The lambda function is necessary here becuase we want Helipad to check the value of the `ratio` parameter each time agents are initialized, since we might change the value between runs. If we simply used the tuple that the lambda function returns as the third argument, it would evaluate the expression using the value of the parameter *when the good was first added*. Since that's before we had a chance to change it in the control panel, it would lock us into the parameter's default value.\n", "\n", "Now it's time to start using [hooks](https://helipad.dev/glossary/hooks/). [`agentInit`](https://helipad.dev/hooks/agentinit/) allows you to hook a function to run each time an agent is initialized. The easiest way to write a hook is to add the `@heli.hook` decorator to a function with the name of the hook." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "@heli.hook\n", "def agentInit(agent, model):\n", "\tagent.utility = CobbDouglas(['shmoo', 'soma'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(Alternatively, we could name the function whatever we want, and decorate it with `@heli.hook('agentInit')`, or add it manually afterward with `heli.hooks.add('agentInit', agentInit)`, but the basic decorator is the easiest way.)\n", "\n", "The `agentInit` hook passes two arguments to its function: the `agent` object – the agent being instantiated – and the general `model` object. Since Helipad takes care of matching and stocks of goods for us, all we need to do here is to give each agent a Cobb-Douglas utility function over two goods, with the default exponents of 0.5." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `match` Function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we’ll move to the meat of the model, steps (3) and (4) above. Since we instantiated a match model above, this will be the [`match`](https://helipad.dev/hooks/match) function, which pairs agents off (if we had set `heli.order` to `'random'` or `'linear'` we would be using the [`agentStep`](https://helipad.dev/hooks/agentstep) hook).\n", "\n", "Before we get to the function, however, we'll need to break out some microeconomics to determine how the two partners will interact. The basic tool to figure out opportunities for gains from trade is called an *Edgeworth Box*. " ] }, { "attachments": { "edgeworth.svg": { "image/svg+xml": [ "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMzY2IDc2OCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTM2NiA3Njg7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7Zm9udC1mYW1pbHk6J0NhbGlicmknO30KCS5zdDF7Zm9udC1zaXplOjYwcHg7fQoJLnN0Mntmb250LXNpemU6MzZweDt9Cgkuc3Qze2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbWl0ZXJsaW1pdDoxMDt9Cgkuc3Q0e2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5OjEyLjA5MzcsMTIuMDkzNzt9Cgkuc3Q1e2ZpbGw6bm9uZTtzdHJva2U6I0Q3MzIyOTtzdHJva2Utd2lkdGg6MztzdHJva2UtbWl0ZXJsaW1pdDoxMDt9Cgkuc3Q2e2ZpbGw6bm9uZTtzdHJva2U6IzJEOERCRTtzdHJva2Utd2lkdGg6MztzdHJva2UtbWl0ZXJsaW1pdDoxMDt9Cgkuc3Q3e2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5OjExLjg2NjcsMTEuODY2Nzt9Cgkuc3Q4e2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5OjEyO30KPC9zdHlsZT4KPGc+Cgk8cGF0aCBkPSJNMTA4NCw4NnY2MDNIMTg4Vjg2SDEwODQgTTEwODcsODNIMTg1djYwOWg5MDJWODNMMTA4Nyw4M3oiLz4KPC9nPgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDExMDAuMjIyNyA4Mi42NjY1KSIgY2xhc3M9InN0MCBzdDEiPk8yPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDExNy4yMjI3IDczNS42NjY1KSIgY2xhc3M9InN0MCBzdDEiPk8xPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgwLjgzODUgLTAuNTQ0OSAwLjU0NDkgMC44Mzg1IDQ4Ny4yMDcyIDQ2NC4zOTcxKSIgY2xhc3M9InN0MCBzdDIiPmNvbnRyYWN0IGN1cnZlPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDYyNy4yMjI3IDU5LjY2NjUpIiBjbGFzcz0ic3QwIHN0MiI+4oaQU29tYTwvdGV4dD4KPHRleHQgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgMSA0NDkuMjIyNyA3MzUuNjY2NSkiIGNsYXNzPSJzdDAgc3QyIj5Tb21h4oaSPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDgwOS4yMjI3IDczNS42NjY1KSIgY2xhc3M9InN0MCBzdDIiPk0xVDwvdGV4dD4KPHRleHQgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgMSAxMDI2LjIyMjcgNzM1LjY2NjUpIiBjbGFzcz0ic3QwIHN0MiI+TTFFPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDEwMTYuMjIyNyA0ODYuNjY2NSkiIGNsYXNzPSJzdDAgc3QyIj5FPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDg0NC4yMjI3IDIyOS42NjY1KSIgY2xhc3M9InN0MCBzdDIiPlQ8L3RleHQ+Cjx0ZXh0IHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIDEgMTAyNi4yMjI3IDcwLjY2NjUpIiBjbGFzcz0ic3QwIHN0MiI+TTJFPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDgxNC4yMjI3IDcwLjY2NjUpIiBjbGFzcz0ic3QwIHN0MiI+TTJUPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDEyOS4yMjI3IDI2My42NjY1KSIgY2xhc3M9InN0MCBzdDIiPkgxVDwvdGV4dD4KPHRleHQgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgMSA1MDYuMjIyNyAxMzcuNjY2NSkiIGNsYXNzPSJzdDAgc3QyIj5VMlQ8L3RleHQ+Cjx0ZXh0IHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIDEgODc1LjIyMjcgMTM3LjY2NjUpIiBjbGFzcz0ic3QwIHN0MiI+VTJFPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDc0OC4yMjI3IDQxMC42NjY1KSIgY2xhc3M9InN0MCBzdDIiPlUxRTwvdGV4dD4KPHRleHQgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgMSA5MjUuMjIyNyAzMTguNjY2NSkiIGNsYXNzPSJzdDAgc3QyIj5VMVQ8L3RleHQ+Cjx0ZXh0IHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIDEgMTI5LjIyMjcgNTEyLjY2NjUpIiBjbGFzcz0ic3QwIHN0MiI+SDFFPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAxIDExMDAuMjIyNyA1MTIuNjY2NSkiIGNsYXNzPSJzdDAgc3QyIj5IMkU8L3RleHQ+Cjx0ZXh0IHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIDEgMTEwMC4yMjI3IDI2My42NjY1KSIgY2xhc3M9InN0MCBzdDIiPkgyVDwvdGV4dD4KPHRleHQgdHJhbnNmb3JtPSJtYXRyaXgoMCAxIC0xIDAgMTEyMi44NTg5IDMxNi42MDc5KSIgY2xhc3M9InN0MCBzdDIiPlNobW9v4oaSPC90ZXh0Pgo8dGV4dCB0cmFuc2Zvcm09Im1hdHJpeCgwIC0xIDEgMCAxNTUuMjgxMSA0MzkuMzAyNykiIGNsYXNzPSJzdDAgc3QyIj5TaG1vb+KGkjwvdGV4dD4KPGc+Cgk8Zz4KCQk8bGluZSBjbGFzcz0ic3QzIiB4MT0iMTg1IiB5MT0iNjkyIiB4Mj0iMTg5Ljk3IiB5Mj0iNjg4LjY0Ii8+CgkJPGxpbmUgY2xhc3M9InN0NCIgeDE9IjIwMCIgeTE9IjY4MS44OCIgeDI9IjEwNzcuMDIiIHkyPSI4OS43NCIvPgoJCTxsaW5lIGNsYXNzPSJzdDMiIHgxPSIxMDgyLjAzIiB5MT0iODYuMzYiIHgyPSIxMDg3IiB5Mj0iODMiLz4KCTwvZz4KPC9nPgo8cGF0aCBjbGFzcz0ic3Q1IiBkPSJNNTU1LjUsMTIxLjVjMjU1LDI2LDM4OCwxNzksNDEwLDUxMSIvPgo8cGF0aCBjbGFzcz0ic3Q1IiBkPSJNNzgwLjUsOTYuNWMxMjEsMjcsMjUxLDE0OCwyNzMsNDgwIi8+CjxwYXRoIGNsYXNzPSJzdDYiIGQ9Ik0xMDY2LjUsMzc3LjVjLTIwMy00Ni0yOTYtMTk1LTMxMi0yODMiLz4KPHBhdGggY2xhc3M9InN0NiIgZD0iTTEwNjYuNSw1MDUuNWMtMjc2LTY2LTQwOC0yMzEtNDA4LTM5OSIvPgo8Y2lyY2xlIGN4PSI4MzgiIGN5PSIyNTEiIHI9IjEzIi8+CjxjaXJjbGUgY3g9IjEwNDYiIGN5PSI1MDAiIHI9IjEzIi8+CjxnPgoJPGc+CgkJPGxpbmUgY2xhc3M9InN0MyIgeDE9IjEwODciIHkxPSI1MDAiIHgyPSIxMDgxIiB5Mj0iNTAwIi8+CgkJPGxpbmUgY2xhc3M9InN0NyIgeDE9IjEwNjkuMTMiIHkxPSI1MDAiIHgyPSIxOTYuOTMiIHkyPSI1MDAiLz4KCQk8bGluZSBjbGFzcz0ic3QzIiB4MT0iMTkxIiB5MT0iNTAwIiB4Mj0iMTg1IiB5Mj0iNTAwIi8+Cgk8L2c+CjwvZz4KPGxpbmUgY2xhc3M9InN0OCIgeDE9IjEwODYuNSIgeTE9IjI1MC41IiB4Mj0iMTg0LjUiIHkyPSIyNTAuNSIvPgo8bGluZSBjbGFzcz0ic3Q4IiB4MT0iODM3LjUiIHkxPSI4Mi41IiB4Mj0iODM3LjUiIHkyPSI2OTEuNSIvPgo8bGluZSBjbGFzcz0ic3Q4IiB4MT0iMTA0NS41IiB5MT0iODIuNSIgeDI9IjEwNDUuNSIgeTI9IjY5MS41Ii8+Cjwvc3ZnPgo=" ] } }, "cell_type": "markdown", "metadata": {}, "source": [ "![edgeworth.svg](attachment:edgeworth.svg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In an Edgeworth box, we plot two agents’ space of two goods, but we invert the second and place it on top of the first. Agent 1’s possessions are counted from the bottom-left axis, and agent 2’s are counted from the top-right axis. The height and width of the box, therefore, represent the total of the two goods between them, and a point in the box – for example, point E – represents four pieces of information: agent 1’s stock of shmoo ($H_1^E$), agent 1’s stock of soma ($M_1^E$), agent 2’s stock of shmoo ($H_2^E$), and agent 2’s stock of soma ($M_2^E$).\n", "\n", "In this example, point E is our *endowment point*. These four values represent the amount of shmoo and soma, respectively, that the agents bring into the trade. Suppose this is period 1, so agent 2 has very little soma ($M_2^E$ is very small), agent 1 has a great deal, and they both have middling amounts of shmoo.\n", "\n", "The blue curves represent the Cobb-Douglas utility function we gave agent 1 earlier. Each curve, what is called an *indifference curve*, indicates all the points on the curve that would give agent 1 the same utility. It slopes downward due to the fact that at any point, agent 1 would be willing to trade *some* amount of soma for additional shmoo. Blue curves further out from O1 indicate higher utility. The same pertains to the red lines for agent 2, except that lines further out from O2 will indicate higher utility.\n", "\n", "At the endowment point E, agents 1 and 2 have utility corresponding to $U_1^E$ and $U_2^E$, respectively. How do we know if they can become better off by trading?\n", "\n", "One result from microeconomics is that any two agents can become more satisfied by trading if their *marginal rates of substitution* between the two goods are different – that is, if the slopes of their indifference curves at E are different. This means that there exists some range of prices where agent 1 would be willing to sell something and agent 2 would be willing to buy it (and vice versa in terms of the other good), and both would be happy with this arrangement (i.e. the indifference curve would be pushed further out).\n", "\n", "Marginal rates of substitution are equal between the two when their indifference curves are tangent to each other. The goal, then, is to find some trade of soma for shmoo that moves the two agents to a point in the Edgeworth box where their indifference curves would be tangent to one another.\n", "\n", "The *contract curve* above is the set of all points in the Edgeworth box where the two curves would be tangent (*note that the contract curve is only a straight line between the two origins when the exponents on the Cobb-Douglas utility function are equal, as we ensured when instantiating the CobbDouglas object above*). In addition, we’ll want to find a point on the contract curve inside the lens made by the indifference curves sprouting from point E, as any other point would make one of them worse off (though if they had started from that point, there would be no further gains from trade).\n", "\n", "Any point on the contract curve inside that lens will do. For our purposes though, we’ll just split the difference. We’ll find the points where $U_1^E$ and $U_2^E$ hit the contract curve, and trade enough soma and shmoo to move to the midpoint between the two – namely, point T, which gives both parties higher utility $U_1^T$ and $U_2^T$.\n", "\n", "We write this logic into the `match` function as follows:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "@heli.hook\n", "def match(agents, primitive, model, stage):\n", "\tu1e = agents[0].utility.calculate(agents[0].stocks)\n", "\tu2e = agents[1].utility.calculate(agents[1].stocks)\n", "\t\n", "\t#Get the endpoints of the contract curve\n", "\t#Contract curve isn't linear unless the CD exponents are both 0.5. If not, *way* more complicated\n", "\tcc1Soma = u1e * (sum(a.stocks['soma'] for a in agents)/sum(a.stocks['shmoo'] for a in agents)) ** 0.5\n", "\tcc2Soma = sum(a.stocks['soma'] for a in agents) - u2e * (sum(a.stocks['soma'] for a in agents)/sum(a.stocks['shmoo'] for a in agents)) ** 0.5\n", "\tcc1Shmoo = sum(a.stocks['shmoo'] for a in agents)/sum(a.stocks['soma'] for a in agents) * cc1Soma\n", "\tcc2Shmoo = sum(a.stocks['shmoo'] for a in agents)/sum(a.stocks['soma'] for a in agents) * cc2Soma\n", "\t\n", "\t#Calculate demand: choose a random point on the contract curve\n", "\tr = random.random()\n", "\tsomaDemand = r*cc1Soma + (1-r)*cc2Soma - agents[0].stocks['soma']\n", "\tshmooDemand = r*cc1Shmoo + (1-r)*cc2Shmoo - agents[0].stocks['shmoo']\n", "\t\n", "\t#Do the trades\n", "\tif abs(somaDemand) > 0.1 and abs(shmooDemand) > 0.1:\n", "\t\tagents[0].trade(agents[1], 'soma', -somaDemand, 'shmoo', shmooDemand)\n", "\t\tagents[0].lastPrice = -somaDemand/shmooDemand\n", "\t\tagents[1].lastPrice = -somaDemand/shmooDemand\n", "\telse:\n", "\t\tagents[0].lastPrice = None\n", "\t\tagents[1].lastPrice = None\n", "\t\n", "\t#Record data\n", "\tagents[0].utils = agents[0].utility.calculate(agents[0].stocks)\n", "\tagents[1].utils = agents[1].utility.calculate(agents[1].stocks)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `match` hook sends four arguments: a list of matched agents (in this case two of them), their [primitive](https://helipad.dev/glossary/#primitive), the model object, and the current model stage. Since we haven't created any new primitives, `primitive` will always equal `'agent'`, the default primitive. We could also have the model run in multiple stages by setting `heli.stages`, but since we haven't done that, `stage` will always equal 1.\n", "\n", "Lines 3-11 find the endpoints of the contract curve. 3, 8, and 10 solve for the point where agent 1’s endowment utility sits on the contract curve. If $U=M^{0.5}H^{0.5}$, then $U_1^E = \\sqrt{M_1^E H_1^E}$, which the `CobbDouglas` object we instantiated earlier as `agent.utility` can calculate automatically. Solving for H and setting it equal to the equation for the contract curve gives us the endpoint, which is denoted by the point (`cc1Soma`, `cc1Shmoo`), which we calculate on lines 8 and 10. An identical process is played out for agent 2 on lines 4, 9, and 11 to find the other endpoint where $U_2^E$ intersects the contract curve.\n", "\n", "Having found these two endpoints, the third block selects a random point on the contract curve and subtracts the existing endowment in order to find the quantities necessary to move from point E to a point T. Geometrically, we can demonstrate that this gives both parties higher utility starting from *any point not already on the contract curve*. Note that, geometrically, in order to get to the contract curve, one of either `somaDemand` and `shmooDemand` will be positive, and the other negative.\n", "\n", "Provided the amounts to be traded aren’t minuscule, line 16 actually executes the trade. The `trade()` method of the agent class transfers `-somaDemand` soma from the agent, and gives `shmooDemand` shmoo to partner. Note that the third and fifth arguments of `trade()` expect a supply and a demand for the two goods from the perspective of the first agent, which means it expects both to be positive or both to be negative (*Note: if one argument were negative and the other positive, one agent would be paying its partner one good in order to take some of the other good. One of these good, therefore, would be a bad rather than a good*). In lines 3-16, however, we calculated the agent’s demand for both goods, one of which will be negative (i.e. a supply of it). We therefore reverse `somaDemand` in the third argument to indicate a supply of soma. \n", "\n", "We also record `lastPrice` and `utils` as properties of each agent, updating every period, in order to collect and display them later. The `if` block tells the agents to record the terms of trade as a `lastPrice` property, *if* they traded. Otherwise, the `else` block indicates that they didn’t trade, and shouldn’t be counted as part of that period’s average. The final two lines record the agents’ utilities from their new endowments of shmoo and soma." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Data Collection and Visualization" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For our purposes, we’ll be interested in keeping track of (1) average utility, (2) the volume of trade each period, and (3) the terms of trade. If we did things correctly, we’ll expect to see (1) utility rising, since agents won’t trade if they don’t become better off; (2) the volume of trade falling, since any two agents that find each other will be closer to the contract curve in later periods than in earlier periods, and (3) convergence on a single price.\n", "\n", "The first thing we need to do is tell Helipad what kind of visualization we want. In this case we'll import `TimeSeries`, which lets us plot data over time. We'll use methods of the `viz` variable later to add plots and series." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from helipad.visualize import TimeSeries\n", "viz = heli.useVisual(TimeSeries)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Any data we plot visually will have to be recorded with a *reporter,* which returns one value per model period. Fortunately Helipad keeps tracks the volume of trade automatically when we use the [`trade()` function](https://helipad.dev/functions/baseagent/trade/) and registers the reporter for us. We've also kept track of utility and terms of trade at the end of our previous function, so now we need reporters to tell Helipad how to aggregate and display those properties. In our case, in addition to the volume of trade, we want Helipad to record *average* utility and *average* price, along with maximum and minimum prices (to see dispersion).\n", "\n", "Helipad already records average utility by default by looking at the `utils` property of agents, so once we’ve given agents a utility in lines 8 an 9 above, the plotting is already taken care of. That leaves just the prices. In order to set up a plot, we’ll have a four-step process:\n", "\n", "1. Aggregate the data each period,\n", "2. Register the reporter so Helipad keeps track of it,\n", "3. Set up a plot on which we can display series together, and\n", "4. Register the reporter as a series, so we can visualize it, and place it on a plot.\n", "\n", "Each of these is associated with a function. For 1, we use [`heli.data.agentReporter()`](https://helipad.dev/functions/data/agentreporter), which cycles over all the agents and computes a statistic based on a property name. We’ll use this in combination with [`heli.data.addReporter()`](https://helipad.dev/functions/data/addreporter) for step 2, which makes sure to record the result of `heli.data.agentReporter` each period and put it in the data output." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "heli.data.addReporter('ssprice', heli.data.agentReporter('lastPrice', 'agent', stat='gmean', percentiles=[0,100]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The second argument of `addReporter` must be a function that takes the model object. `agentReporter` generates such a function that calculates the geometric mean of the value of the `lastPrice` property of each agent (Geometric mean, rather than arithmetic mean, is appropriate for relative prices as 0.1 should be an equal distance from 1 as 10), along with additional plots for the 0th and 100th percentiles (maximum and minimum values). This column of the data can then be plotted and referred back to in Helipad’s data functions with the name `'ssprice'`.\n", "\n", "In order to visualize our data in real time, we’ll need somewhere to put it – step 3. This is called a plot, and is registered using the [`addPlot`](https://helipad.dev/functions/model/addplot) method of our visualization class that we stored in the `viz` variable. Finally, for step 4, we’ll create a series, which tells Helipad to display a reporter on the plot we specify." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pricePlot = viz.addPlot('price', 'Price', logscale=True, selected=True)\n", "pricePlot.addSeries('ssprice', 'Soma/Shmoo Price', '#119900')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Line 1 gives us a place to put our price series, lets us refer to it as `‘price’` and labels it `‘Price’`. Since we’re looking at a price ratio, for the same reason we used a geometric mean, we’ll also want to display it on a logarithmic scale. Finally, line 2 tells Helipad to use the `ssprice` reporter we set up earlier and draw it on the `'price'` plot from the first line, label it ‘Soma/Shmoo Price’, and color it `#119900`, which is a medium-dark green.\n", "\n", "All of the data registered as a reporter can be accessed algorithmically within the model, or after the model runs, to integrate with the statistical techniques from the previous chapter. The entire data output can be accessed as a Pandas dataframe using [`heli.data.dataframe`](https://helipad.dev/functions/data/#dataframe). Particular values of a series can be accessed using [`heli.data.getLast()`](https://helipad.dev/functions/data/getlast) (see below)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Final Touches" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The model as it stands is ready to run. Just a few more niceties before finishing. First, because this model is one of a decentralized convergence to equilibrium, it won’t be interesting to keep running it once it gets sufficiently close to equilibrium.\n", "\n", "Helipad has a built-in configuration parameter `'stopafter'` that displays as a [checkentry](https://helipad.dev/functions/checkentry/) in the control panel. If `stopafter` is `False`, the model runs forever. If `stopafter` is an integer, the model stops after that many periods.\n", "\n", "However, `stopafter` can also be set to a string that points to an [*event*](https://helipad.dev/functions/events/add/) name, in order for us to establish more complex stopping conditions. Events are items that trigger when a certain criterion is satisfied. Ordinarily they draw a vertical line on our plot function, but here we want to *stop* the model when the criterion is satisfied." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "#Stop the model when we're basically equilibrated\n", "@heli.event\n", "def stopCondition(model):\n", "\treturn model.t > 1 and model.data.getLast('demand-shmoo') < 20 and model.data.getLast('demand-soma') < 20\n", "heli.param('stopafter', 'stopCondition')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this case, we stop the model when (1) the current time is greater than 1, (2) when the total shmoo traded is less than 20, and (3) when the total soma traded is also less than 20. Given that we gave each agent a random endowment of up to 1000 of each, and that the default number of agents is 50, a total volume of trade under 20 is quite low, comparatively. We write a function that returns `False` until all three of these things are true, then register it as an event with the [`@heli.event` decorator](https://helipad.dev/functions/model/event/). Then, we set the `stopafter` parameter to the name of the event we registered.\n", "\n", "Finally, a line to make sure only the plots with actual series are selected by default in the control panel." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "for p in ['demand', 'utility']: viz[p].active(True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We also set the refresh rate parameter to 1 in order to see it play out live. Ordinarily this would make the model quite slow, but because it equilibrates so quickly (less than 20 periods), the default refresh rate of 20 would just show the final result." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "heli.param('refresh', 1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And with that, our model is complete! Launch the control panel if you want to adjust the parameters visually before running the model. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "heli.launchCpanel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we've set our parameters to our liking, we can actually run the model. [`heli.start()`](https://helipad.dev/functions/model/start/) will run the model without the plots; this starts the model along with the plotting." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "scrolled": false }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "heli.launchVisual()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From this output, it looks like all three of our predictions were validated: (1) utility is rising as agents accomplish more trade, (2) price dispersion narrows as agents converge on an equilibrium price, and (3) the volume of trade declines as agents get closer and closer to equilibrium. It only takes about 15 periods to get a total volume of trade below 20!\n", "\n", "From here you can explore the model in various ways:\n", "\n", "1. Adjust the parameters in the control panel and re-run the model by running the last line again. See how the endowment ratio affects the equilibrium price, for example.\n", "2. Click on the top plot with the `demand` series and press 'L' to toggle a logarithmic scale on the vertical axis. The resulting lines are approximately linear, meaning the demand function with respect to time takes the form *e*-*t*.\n", "3. Add other settings to see how they affect the model. For example, add a slider parameter to control the probability that an agent trades in a given period (i.e. only execute the `match` function with a certain probability). If only half of agents trade each period, for example, the convergence to equilibrium would be slower. Or you might split the difference on the contract curve differently, to see how differences in bargaining power affect the process of equilibration.\n", "\n", "One final note: all this code will also work as a standalone Python app with a Tkinter frontend if you run it as a file. The only difference is that the last line `heli.launchVisual()` is unnecessary outside of a Jupyter notebook, as the Tkinter control panel provides a button to launch the plots.\n", "\n", "[**See the model code put together ▸**](https://github.com/charwick/helipad/blob/master/sample-models/pricediscover.py)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "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.10.5" } }, "nbformat": 4, "nbformat_minor": 1 }