{
"metadata": {
"name": "",
"signature": "sha256:70ec45227d433d5a709cbe48cd91655b7095a5ec838bd1b606ff430fb7db0985"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "heading",
"level": 1,
"metadata": {},
"source": [
"Lesson 16. SimPy — levels"
]
},
{
"cell_type": "heading",
"level": 3,
"metadata": {},
"source": [
"SA421 Fall 2014"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"##### Setup #####\n",
"# Friendly floating-point division\n",
"from __future__ import division\n",
"\n",
"# Import infinity from NumPy\n",
"from numpy import inf\n",
"\n",
"# Import seed initializer and random sampling functions from NumPy\n",
"from numpy.random import seed, rand\n",
"\n",
"# Import bisect_right from bisect\n",
"from bisect import bisect_left\n",
"\n",
"# Import all simulation functions from SimPy\n",
"from SimPy.Simulation import *"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 1
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Today's problem"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The Simplex Bakery is trying to determine how many dozens of [cronuts](http://en.wikipedia.org/wiki/Cronut) to bake each day. \n",
"The probability distribution of the number of cronut customers is as follows:\n",
"\n",
"| Number of customers per day | 5 | 10 | 15 | 20 |\n",
"| --------------------------- | ---- | ---- | ---- | ---- |\n",
"| Probability | 0.15 | 0.40 | 0.35 | 0.10 |\n",
"\n",
"Customers order 1, 2, 3 or 4 cronuts according to the following probability distribution:\n",
"\n",
"| Number of dozen ordered per customer | 1 | 2 | 3 | 4 |\n",
"| ------------------------------------ | --- | --- | --- | --- |\n",
"| Probability | 0.3 | 0.4 | 0.2 | 0.1 |\n",
"\n",
"Cronuts sell for \\$3.50 per dozen. \n",
"They cost \\$2.00 per dozen to make. \n",
"All cronuts not sold at the end of the day are sold at half price to a local stale baked good distributor.\n",
"\n",
"Due to mixing size constraints, cronuts must be made in batches of 10 dozen.\n",
"Currently, the bakery bakes 20 dozen.\n",
"\n",
"* Based on a 1-day simulation, what is the bakery's profit? How many customers are left unsatisfied (didn't get the number of cronuts wanted)?\n",
"\n",
"\n",
"* Based on a 1-day simulation, how many dozen should the bakery bake to maximize its profit?"
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Warm up"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Write a function called `customersPerDayDist` that samples from the probability distribution on the number of customers that the Simplex Bakery has per day, described above."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Distributions: number of customers per day\n",
"def customersPerDayDist():\n",
" # Sorted list of possible values and -inf\n",
" a = [-inf, 5, 10, 15, 20]\n",
"\n",
" # cdf at each possible value\n",
" cdf = [0, 0.15, 0.55, 0.90, 1]\n",
"\n",
" # Generate variate\n",
" variate = a[bisect_left(cdf, rand())]\n",
"\n",
" # Return generated variate\n",
" return variate"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 2
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Write a function called `cronutsPerCustomerDist` that samples from the probability distribution on the number of cronuts each customer orders, described above."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Distributions: number of cronuts per customer\n",
"def cronutsPerCustomerDist():\n",
" # Sorted list of possible values and -inf\n",
" a = [-inf, 1, 2, 3, 4]\n",
"\n",
" # cdf at each possible value\n",
" cdf = [0, 0.30, 0.70, 0.90, 1]\n",
"\n",
" # Generate variate\n",
" variate = a[bisect_left(cdf, rand())]\n",
"\n",
" # Return generated variate\n",
" return variate"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 3
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Levels"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* We need a way to model the number of cronuts available at any given time.\n",
"\n",
"\n",
"* A **level** in SimPy represents inventory of a homogeneous, undifferentiated \"material\".\n",
"\n",
"\n",
"* As with resources, we will set up a class for levels for consistent naming purposes. \n",
"\n",
"\n",
"* Also as with resources, we will not actually create the SimPy level object until we define the `model()` function that runs the simulation."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"##### Levels #####\n",
"class L:\n",
" cronuts = None"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 4
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"The rest of the SimPy model"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* First, let's define input parameters for our simulation."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"##### Parameters #####\n",
"class P:\n",
" # Revenue per dozen cronuts\n",
" revenue = 3.5\n",
" \n",
" # Cost per dozen cronuts\n",
" cost = 2.0\n",
" \n",
" # Number of dozen cronuts baked\n",
" nCronuts = 20"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 5
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Next, like with our other SimPy simulations, we need to define processes for our simulation model.\n",
"\n",
"\n",
"* Let's start by defining a `Customer` process that simulates the behavior of a customer.\n",
"\n",
"\n",
"* To get `quantity` units of material to a level, say `L.cronuts`, we use:\n",
"```\n",
"yield get, self, L.cronuts, quantity\n",
"```\n",
"\n",
"* To add `quantity` units of material to a level, again, say `L.cronuts`, we use:\n",
"```\n",
"yield put, self, L.cronuts, quantity\n",
"```\n",
"\n",
"* `L.cronuts.amount` gives the current amount held in the level `L.cronuts`."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Customer\n",
"class Customer(Process):\n",
" def behavior(self):\n",
" # Customer arrives\n",
" print(\"{0} arrives\".format(self.name))\n",
" \n",
" # Customer determines how many cronuts she wants\n",
" cronutsWanted = cronutsPerCustomerDist()\n",
" print(\"{0} wants {1} dozen\".format(self.name, cronutsWanted))\n",
" print(\"There are {0} dozen available\".format(L.cronuts.amount))\n",
" \n",
" # Are there enough cronuts for this customer?\n",
" if cronutsWanted > L.cronuts.amount:\n",
" print(\"{0} gets {1} dozen, is not satisfied\".format(self.name, L.cronuts.amount))\n",
" yield get, self, L.cronuts, L.cronuts.amount\n",
" else:\n",
" print(\"{0} gets {1} dozen, is satisfied\".format(self.name, cronutsWanted))\n",
" yield get, self, L.cronuts, cronutsWanted"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 36
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Let's also define an `Entrance` process that models the arrival of customers to the bakery."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Entrance\n",
"class Entrance(Process):\n",
" def behavior(self):\n",
" # Determine how many customers will arrive at the bakery today\n",
" nCustomersTotal = customersPerDayDist()\n",
" \n",
" nCustomersArrived = 0\n",
" while nCustomersArrived < nCustomersTotal:\n",
" # Create customer using the template defined in the Customer class\n",
" c = Customer(name = \"Customer {0}\".format(nCustomersArrived))\n",
" \n",
" # Activate the customer's behavior\n",
" activate(c, c.behavior())\n",
" \n",
" # Count this new customer\n",
" nCustomersArrived += 1\n",
" \n",
" # Dummy yield hold statement\n",
" yield hold, self, 0"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 42
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Note that processes in SimPy need at least 1 `yield` statement of some kind; hence the dummy `yield hold` statement above."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Finally, let's create the `model()` function we customarily use to initialize SimPy, create any resources, levels, and monitors, and run the simulation.\n",
"\n",
"\n",
"* Let's have the `model()` function take as input the seed we want to use to initialize the random number generator.\n",
"\n",
"\n",
"* In addition, let's have the `model()` function output, or return, the resulting profit of the day."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def model(inputSeed):\n",
" # Initialize SimPy\n",
" initialize()\n",
" \n",
" # Initialize a seed for the random number generator\n",
" seed(inputSeed)\n",
"\n",
" # Create level for cronuts\n",
" L.cronuts = Level(initialBuffered = P.nCronuts)\n",
" \n",
" # Activate the entrance\n",
" e = Entrance()\n",
" activate(e, e.behavior())\n",
" \n",
" # Run the simulation\n",
" simulate()\n",
"\n",
" # Compute profit\n",
" profit = (P.revenue - P.cost) * (P.nCronuts - L.cronuts.amount) + (0.5 * P.revenue - P.cost) * L.cronuts.amount\n",
" \n",
" # Return performance measures\n",
" return profit"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 38
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* A level is created by setting the level's variable name to a call of the `Level()` function.\n",
" - `initialBuffered` is the initial quantity of material in the level when it is created.\n",
" \n",
"\n",
"* Note that the `until` keyword is omitted when calling `simulate()`: no time limit is needed in this simulation.\n",
"\n",
"\n",
"* Let's run the model and see what happens."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"model(inputSeed = 123)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Customer 0 arrives\n",
"Customer 0 wants 1 dozen\n",
"There are 20 dozen available\n",
"Customer 0 gets 1 dozen, is satisfied\n",
"Customer 1 arrives\n",
"Customer 1 wants 1 dozen\n",
"There are 19 dozen available\n",
"Customer 1 gets 1 dozen, is satisfied\n",
"Customer 2 arrives\n",
"Customer 2 wants 2 dozen\n",
"There are 18 dozen available\n",
"Customer 2 gets 2 dozen, is satisfied\n",
"Customer 3 arrives\n",
"Customer 3 wants 3 dozen\n",
"There are 16 dozen available\n",
"Customer 3 gets 3 dozen, is satisfied\n",
"Customer 4 arrives\n",
"Customer 4 wants 2 dozen\n",
"There are 13 dozen available\n",
"Customer 4 gets 2 dozen, is satisfied\n",
"Customer 5 arrives\n",
"Customer 5 wants 4 dozen\n",
"There are 11 dozen available\n",
"Customer 5 gets 4 dozen, is satisfied\n",
"Customer 6 arrives\n",
"Customer 6 wants 2 dozen\n",
"There are 7 dozen available\n",
"Customer 6 gets 2 dozen, is satisfied\n",
"Customer 7 arrives\n",
"Customer 7 wants 2 dozen\n",
"There are 5 dozen available\n",
"Customer 7 gets 2 dozen, is satisfied\n",
"Customer 8 arrives\n",
"Customer 8 wants 2 dozen\n",
"There are 3 dozen available\n",
"Customer 8 gets 2 dozen, is satisfied\n",
"Customer 9 arrives\n",
"Customer 9 wants 2 dozen\n",
"There are 1 dozen available\n",
"Customer 9 gets 1 dozen, is not satisfied\n",
"Customer 10 arrives\n",
"Customer 10 wants 3 dozen\n",
"There are 0 dozen available\n",
"Customer 10 gets 0 dozen, is not satisfied\n",
"Customer 11 arrives\n",
"Customer 11 wants 2 dozen\n",
"There are 0 dozen available\n",
"Customer 11 gets 0 dozen, is not satisfied\n",
"Customer 12 arrives\n",
"Customer 12 wants 1 dozen\n",
"There are 0 dozen available\n",
"Customer 12 gets 0 dozen, is not satisfied\n",
"Customer 13 arrives\n",
"Customer 13 wants 2 dozen\n",
"There are 0 dozen available\n",
"Customer 13 gets 0 dozen, is not satisfied\n",
"Customer 14 arrives\n",
"Customer 14 wants 3 dozen\n",
"There are 0 dozen available\n",
"Customer 14 gets 0 dozen, is not satisfied\n"
]
},
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 43,
"text": [
"30.0"
]
}
],
"prompt_number": 43
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Note that you can integrate levels with resources in a SimPy model to model complex situations where queueing and inventory interact."
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"With a neighbor..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Implement a monitor called `M.satisfied` that records for each customer:\n",
" - 1 if the customer receives his/her entire order\n",
" - 0 otherwise.\n",
" \n",
" \n",
"* Use this monitor to compute the fraction of customers that are left unsatisfied."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* First, let's:\n",
" - establish a class `M` to hold our monitors,\n",
" - define a dummy variable for `M.satisfied`, and \n",
" - redefine `model()` to activate the monitor."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"class M:\n",
" satisfied = None\n",
" \n",
"def model(inputSeed):\n",
" # Initialize SimPy\n",
" initialize()\n",
" \n",
" # Initialize a seed for the random number generator\n",
" seed(inputSeed)\n",
"\n",
" # Create level for cronuts\n",
" L.cronuts = Level(initialBuffered = P.nCronuts)\n",
" \n",
" # Create monitor for satisfaction\n",
" M.satisfied = Monitor()\n",
" \n",
" # Activate the entrance\n",
" e = Entrance()\n",
" activate(e, e.behavior())\n",
" \n",
" # Run the simulation\n",
" simulate()\n",
"\n",
" # Compute profit\n",
" profit = (P.revenue - P.cost) * (P.nCronuts - L.cronuts.amount) + (0.5 * P.revenue - P.cost) * L.cronuts.amount\n",
" \n",
" # Return performance measures\n",
" return profit"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 44
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Next, let's modify the customer behavior so that the monitor observes the appropriate values."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Customer\n",
"class Customer(Process):\n",
" def behavior(self):\n",
" # Customer arrives\n",
" print(\"{0} arrives\".format(self.name))\n",
" \n",
" # Customer determines how many cronuts she wants\n",
" cronutsWanted = cronutsPerCustomerDist()\n",
" print(\"{0} wants {1} dozen\".format(self.name, cronutsWanted))\n",
" print(\"There are {0} dozen available\".format(L.cronuts.amount))\n",
" \n",
" # Are there enough cronuts for this customer?\n",
" if cronutsWanted > L.cronuts.amount:\n",
" print(\"{0} gets {1} dozen, is not satisfied\".format(self.name, L.cronuts.amount))\n",
" yield get, self, L.cronuts, L.cronuts.amount\n",
" M.satisfied.observe(0)\n",
" else:\n",
" print(\"{0} gets {1} dozen, is satisfied\".format(self.name, cronutsWanted))\n",
" yield get, self, L.cronuts, cronutsWanted\n",
" M.satisfied.observe(1)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 45
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Finally, let's run the model and compute the desired performance measure."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"model(inputSeed = 123)\n",
"print(\"Fraction of customers unsatisfied = {0}\".format(1 - M.satisfied.mean()))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Customer 0 arrives\n",
"Customer 0 wants 1 dozen\n",
"There are 20 dozen available\n",
"Customer 0 gets 1 dozen, is satisfied\n",
"Customer 1 arrives\n",
"Customer 1 wants 1 dozen\n",
"There are 19 dozen available\n",
"Customer 1 gets 1 dozen, is satisfied\n",
"Customer 2 arrives\n",
"Customer 2 wants 2 dozen\n",
"There are 18 dozen available\n",
"Customer 2 gets 2 dozen, is satisfied\n",
"Customer 3 arrives\n",
"Customer 3 wants 3 dozen\n",
"There are 16 dozen available\n",
"Customer 3 gets 3 dozen, is satisfied\n",
"Customer 4 arrives\n",
"Customer 4 wants 2 dozen\n",
"There are 13 dozen available\n",
"Customer 4 gets 2 dozen, is satisfied\n",
"Customer 5 arrives\n",
"Customer 5 wants 4 dozen\n",
"There are 11 dozen available\n",
"Customer 5 gets 4 dozen, is satisfied\n",
"Customer 6 arrives\n",
"Customer 6 wants 2 dozen\n",
"There are 7 dozen available\n",
"Customer 6 gets 2 dozen, is satisfied\n",
"Customer 7 arrives\n",
"Customer 7 wants 2 dozen\n",
"There are 5 dozen available\n",
"Customer 7 gets 2 dozen, is satisfied\n",
"Customer 8 arrives\n",
"Customer 8 wants 2 dozen\n",
"There are 3 dozen available\n",
"Customer 8 gets 2 dozen, is satisfied\n",
"Customer 9 arrives\n",
"Customer 9 wants 2 dozen\n",
"There are 1 dozen available\n",
"Customer 9 gets 1 dozen, is not satisfied\n",
"Customer 10 arrives\n",
"Customer 10 wants 3 dozen\n",
"There are 0 dozen available\n",
"Customer 10 gets 0 dozen, is not satisfied\n",
"Customer 11 arrives\n",
"Customer 11 wants 2 dozen\n",
"There are 0 dozen available\n",
"Customer 11 gets 0 dozen, is not satisfied\n",
"Customer 12 arrives\n",
"Customer 12 wants 1 dozen\n",
"There are 0 dozen available\n",
"Customer 12 gets 0 dozen, is not satisfied\n",
"Customer 13 arrives\n",
"Customer 13 wants 2 dozen\n",
"There are 0 dozen available\n",
"Customer 13 gets 0 dozen, is not satisfied\n",
"Customer 14 arrives\n",
"Customer 14 wants 3 dozen\n",
"There are 0 dozen available\n",
"Customer 14 gets 0 dozen, is not satisfied\n",
"Fraction of customers unsatisfied = 0.4\n"
]
}
],
"prompt_number": 46
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Optimizing cronut production"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Finally, let's determine the number of cronuts the Simplex Bakery needs to produce to maximize its profits.\n",
"\n",
"\n",
"* First, let's modify the `Customer` process so that it does not print all the details."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Customer\n",
"class Customer(Process):\n",
" def behavior(self):\n",
" # Customer arrives\n",
" # Customer determines how many cronuts she wants\n",
" cronutsWanted = cronutsPerCustomerDist()\n",
" \n",
" # Are there enough cronuts for this customer?\n",
" if cronutsWanted > L.cronuts.amount:\n",
" yield get, self, L.cronuts, L.cronuts.amount\n",
" else:\n",
" yield get, self, L.cronuts, cronutsWanted"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 47
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Now, let's run the model for differing numbers of cronuts baked."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"for P.nCronuts in [0, 10, 20, 30, 40, 50, 60, 70, 80]:\n",
" profit = model(inputSeed = 123)\n",
" print(\"Dozens baked = {0}, profit = {1}\".format(P.nCronuts, profit))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Dozens baked = 0, profit = 0.0\n",
"Dozens baked = 10, profit = 15.0\n",
"Dozens baked = 20, profit = 30.0\n",
"Dozens baked = 30, profit = 45.0\n",
"Dozens baked = 40, profit = 46.0\n",
"Dozens baked = 50, profit = 43.5\n",
"Dozens baked = 60, profit = 41.0\n",
"Dozens baked = 70, profit = 38.5\n",
"Dozens baked = 80, profit = 36.0\n"
]
}
],
"prompt_number": 50
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* So, based on 1 replication, the Simplex Bakery should bake 40 dozen cronuts."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [],
"language": "python",
"metadata": {},
"outputs": []
}
],
"metadata": {}
}
]
}