Today I took a step back to slightly simpler version of the simulation that uses the original version of the Path components (the ones that have the nodes built into them and act like standalone components). I took two steps forward, however, in that I added new complexity to the framework by providing the ability to include multiple entry points and the ability to either step through the simulation or run it so it steps through the simulation automatically. I also added the ability to pause the simulation when it’s running automatically.
Up to now I’ve assumed that components can only be linked together in one line; each component was linked to exactly one subsequent and/or one previous component. I started to allow for more arbitrary constructions when I developed separate nodes that could be linked to multiple incoming or outgoing paths. We aren’t using independent nodes here so we have to recreate that capability using the components we do have. We can start by allowing the Arrivals component to be linked to multiple Entry components. You can see this in the demo; there is one Arrivals component and two Entry components. Each Entry component is linked to a separate Path component and each Path component is then linked to the single Exit component. It’s a pretty simple setup, but it includes everything needed to illustrate the principle.
The code for the Arrivals component includes a few changes. We’ve eliminated the single value for nextComponent
and replaced it with an array of values called entryList
. This allows the Arrivals component to be linked to as many Entry components as desired.
We’ve also added an additional input parameter to the constructor or initial call, in the form of an entryDistributionArray
. This array should have as many primary members as there are time slices in the model (we’ve been going with twelve time slices of thirty time units each). Each member of the array should itself be an array with as many members as there are linked Entry components. This information is used in the forwardEntity
method in the following way. The values in the secondary dimension of the array represent the cumulative chance that any given entity arrives at that Entry component, ranging from 0.0 to 1.0 and always increasing. We initialize each array to have values of 0.7 and 1.0, which means that 70 percent of the entities will be forwarded to the first Entry component while the other 30% will be forwarded to the second Entry component. If we had assigned five possible Entry components we might specify a secondary array of [0.4,0.45,0.6,0.9,1.0], which means that Entry components one through five would be expected to receive 40, 5, 15, 30, and 10 percent of the generated entries, respectively. The actual selection is based on the result of a call to Math.random()
, which generates a value between 0.0 (inclusive) and 1.0 (not inclusive).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
//Arrivals component: generates entities that will be processed by the model function ArrivalsComponent(scheduleBlockMinutes,scheduleArray,entryDistributionArray) { setOfComponents.push(this); this.componentID = getNewID(); //this.componentName = "Arrivals"; this.componentName = ""; this.startBlockTime = globalSimClock; this.currentBlockTime = globalSimClock; this.incrementTime = scheduleBlockMinutes; this.endBlockTime = globalSimClock + this.incrementTime; this.entitiesScheduled = 0; this.entitiesRemaining = 0; this.currentEntityID = ""; this.activity = "creation"; this.scheduleIndex = -1; this.endTime = endSimTime; this.entryList = []; this.entryCount = 0; //this.nextState = "increment"; feq.newItem(globalSimClock,this,"increment"); //assume all components created at time zero //this.dataGroup = new DisplayGroup(this.componentName,10,10,80,"#00FFFF","#FF0000","#FFFF00"); this.dataGroup = new DisplayGroup1(); this.defineDataGroup = function(x,y,vw,bc,vc,lc) { this.dataGroup.define(this.componentName,x,y,vw,bc,vc,lc); }; this.dataGroup.addValue(this.componentID,"Comp. ID","integer"); this.dataGroup.addValue(this.startBlockTime,"Start Time","numdec",5); this.dataGroup.addValue(this.currentBlockTime,"CurrentTime","numdec",5); this.dataGroup.addValue(this.endBlockTime,"End Time","numdec",5); this.dataGroup.addValue(this.entitiesScheduled,"# Entities","integer"); this.dataGroup.addValue(this.entitiesRemaining,"# Remaining","integer"); this.dataGroup.addValue(this.currentEntityID,"Entity ID","integer"); this.dataGroup.addValue(this.activity,"Activity","text"); this.assignDisplayValues = function() { this.dataGroup.valueList[0].value = this.componentID; this.dataGroup.valueList[1].value = this.startBlockTime; this.dataGroup.valueList[2].value = this.currentBlockTime; this.dataGroup.valueList[3].value = this.endBlockTime; this.dataGroup.valueList[4].value = this.entitiesScheduled; this.dataGroup.valueList[5].value = this.entitiesRemaining; this.dataGroup.valueList[6].value = this.currentEntityID; this.dataGroup.valueList[7].value = this.activity; }; this.drawData = function() { this.assignDisplayValues(); this.dataGroup.drawBasic(); }; this.addToEntryList = function(entry) { this.entryList.push(entry); this.entryCount++; }; this.forwardEntity = function(entity) { //add test to ensure nextComponent is actually assigned var dest = 0; var test = Math.random(); while (test > entryDistributionArray[this.scheduleIndex][dest]) { dest++; } if (dest > this.entryCount) { alert("Arrival comp. tried to assign to entry with too high of an index"); } displayProgressText("Arrivals comp. "+this.componentID+" test: "+test.toFixed(4)+" vs. "+entryDistributionArray[this.scheduleIndex][dest]); this.entryList[dest].receiveEntity(entity); }; this.makeEntity = function() { //var e = generateNewEntity(this); var e = generateNewEntity(); this.currentBlockTime = globalSimClock; this.entitiesRemaining--; this.currentEntityID = e.entityID; //should create function to get this this.activity = "make entity"; displayProgressText("Arrivals comp. "+this.componentID+" generates entity: "+this.currentEntityID+" at time "+globalSimClock.toFixed(6)); //do something to send it to the entry component this.forwardEntity(e); //more to generate if (this.entitiesRemaining > 0) { var nextIndex = this.entitiesScheduled-this.entitiesRemaining; var nextTime = this.arrivalArray[nextIndex]; var thisIndex = nextIndex-1; var thisTime = this.arrivalArray[thisIndex]; advance(nextTime-thisTime,"makeEntity"); //advance(arrivalArray[this.entitiesScheduled-this.entitiesRemaining]-arrivalArray[this.entitiesScheduled-this.entitiesRemaining-1],"makeEntity"); } }; //this.makeEntity this.blockIncrement = function() { //should be called at the beginning of every time block //get arrival count per array index this.scheduleIndex++; //starts from -1, updates at beginning of each window so has same value through duration of window this.entitiesScheduled = scheduleArray[this.scheduleIndex]; if (this.entitiesScheduled > 0) { //var arrivalArray = []; this.arrivalArray = []; for (var i=0; i<this.entitiesScheduled; i++) { this.arrivalArray[i] = Math.random() * scheduleBlockMinutes; } //sort the array this.arrivalArray.sort(compareNumeric); //generate the arrivals //for (var i=0; i<this.entitiesScheduled; i++) { // this.generateNewEntity(this,globalSimClock + this.arrivalArray[i]); //} feq.newItem(globalSimClock+this.arrivalArray[0],this,"makeEntity"); //assume all components created at time zero } displayProgressText("Arrivals comp. "+this.componentID+" generates "+this.entitiesScheduled+" new entities at time "+globalSimClock.toFixed(6)); //set values for display------------- this.startBlockTime = globalSimClock; this.currentBlockTime = globalSimClock; this.endBlockTime = globalSimClock + this.incrementTime; this.entitiesRemaining = this.entitiesScheduled; this.currentEntityID = ""; this.activity = "generate"; //---------------------------------- if (globalSimClock + this.incrementTime >= this.endTime) { //this.nextState = "destroy"; advance(this.incrementTime,"destroy"); } else { advance(this.incrementTime,"increment"); } }; //this.blockIncrement this.destroy = function() { displayProgressText("Arrivals comp. "+this.componentID+" terminates at time "+globalSimClock.toFixed(6)); //do something to take this element out of the global element list - setOfComponents }; //this.destroy this.activate = function(nextState) { if (nextState == "makeEntity") { this.makeEntity(); } else if (nextState == "increment") { this.blockIncrement(); } else if (nextState == "destroy") { this.destroy(); } else { errorUndefinedAdvanceState(this.entityID,nextState); } //clearCanvas("#000000"); //this.drawData(); } } //ArrivalsComponent |
That’s really all there is to it. At this point none of the other components had to be changed. That said, I did add a counter to each Entry component to keep track of the number of arriving entities routed to each. Since I assigned values of 0.7 and 1.0 for each of the 12 time slices we would expect roughly two-thirds of the entities to arrive through the first (left hand) entry components. Since the results are generated randomly and the browser (I tested this only on Firefox and Safari) doesn’t reset the random number seeds when I refresh the page to rerun the model, the exact distribution varies a little bit between runs.
Here’s the code that initializes everything, including the division of arrivals by Entry component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//schedule for six hours of arrivals in half-hour blocks var arrivalSchedule = [0,0,1,3,4,4,5,6,3,2,1,0]; var entryDistribution = [[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0],[0.7,1.0]]; var arrival1 = new ArrivalsComponent(30.0,arrivalSchedule,entryDistribution); arrival1.defineDataGroup(90,10,80,"#00FFFF","#FF0000","#FFFF00"); var entry1 = new EntryComponent(2.0); entry1.defineDataGroup(10,115,80,"#00FFFF","#FF0000","#FFFF00"); var entry2 = new EntryComponent(2.0); entry2.defineDataGroup(175,115,80,"#00FFFF","#FF0000","#FFFF00"); arrival1.addToEntryList(entry1); arrival1.addToEntryList(entry2); var path1 = new PathComponent(); path1.setStartPoint(90,190); path1.setEndPoint(168,250); path1.setSpeedTime(7.5,1.0); path1.calcPathLength(); entry1.assignNextComponent(path1); var path2 = new PathComponent(); path2.setStartPoint(245,190); path2.setEndPoint(168,250); path2.setSpeedTime(7.5,1.0); path2.calcPathLength(); entry2.assignNextComponent(path2); var exit1 = new ExitComponent(2.0); exit1.defineDataGroup(90,250,80,"#00FFFF","#FF0000","#FFFF00"); path1.assignNextComponent(exit1); path2.assignNextComponent(exit1); |
We’ve assigned the diversion percentages for the arrivals (from the Arrivals component to the various Entry components) by time slice but there may not be a need to add that much complexity. If the division by entry point doesn’t change over time then a single set of values could be used just as easily. I’ve worked with some very complex models that made this assumption.
Tomorrow I’ll continue adding complexity by adding the ability to specify multiple destinations to at least one other component type.
Finally, I will also see if I can’t allow the user to reinitialize and rerun the simulation without having to refresh the whole browser page.