Today I modified the framework to define Node components separately from Path components. This allows us to have any number of paths going into or out of any node so that any arbitrary network can be defined. In the code’s current state components that are not nodes or paths can hand entities off to other components that also aren’t nodes or paths, or to nodes. Nodes can hand entities off to paths or to components that aren’t nodes or paths. Paths can on receive entities from nodes and hand entities off to nodes.
This is actually a little bit awkward, and we are on a bridge between two versions of what this kind of discrete-event simulation model can be. One version is more abstract and considers only how entities move from component to component. It might not be very interesting visually, but it would be very fast and would get the job done as far as performing the desired analysis. The other version is more detailed and would effectively do away with all components that are not nodes or paths. In this version the process activities would take place at nodes and queues would form wherever nodes backed up behind processes. That can make it a bit trickier to figure out when an entity entered a given queue but that’s just a technicality.
The more detailed version is much more complex to write and debug and takes more memory and execution time (potentially by a lot) but if way more visually interesting. Observers can see exactly what’s going on inside the model as it runs (or as a run is replayed). For the time being I’m going to back up and finish building out the more abstract version. To give it some visual interest, however, I will retain the visual paths between components so users can get more of a feel for where entities are being sent in a simulation. I might back the representation of the nodes and paths to the earlier version where I defined paths with nodes included on each end, and ensure that I never include more than one path between components (if possible).
There’s a lot that can be done with a more abstract version of this tool and I have an interesting in getting something more interesting running and usable. I plan to do the more interesting and detailed version going forward, and that will involve finding detailed solutions to the problems of entities competing to pass through intersections, as has been discussed over the last couple of days. I think it’ll be helpful to build that kind of framework and play around with it rather than try to whiteboard everything up front where it’s hard to follow what might be going on.
In the meantime here is the code for the new Node component. Notice that there are currently no discrete-event handling mechanisms in this version. When a component (path or otherwise) hands an entity off to the node it immediately gets forwarded to the next component. This may not be enough in the long run, but it works well enough in my test case so it is sufficient for now. The node can be connected to one or more paths or a single non-path component either coming in or going out, so the node figures out the kind of forwarding or receiving to do be checking against the number of incoming or outgoing paths that are linked to the node. Also, when the end of a path is assigned to a node the assignment causes some relevant values to be assigned within the path 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 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 |
function NodeComponent(x,y) { setOfComponents.push(this); this.componentID = getNewID(); this.componentName = "Node"; this.xLoc = x; this.yLoc = y; this.incomingPaths = []; this.outgoingPaths = []; this.previousComponent = null; this.nextComponent = null; this.permissionID = -1; this.firstArrivalTime = Infinity; this.entity = null; this.getComponentID =function() { return this.componentID; }; this.getComponentName =function() { return this.componentName; }; this.assignIncomingPath = function(component) { if (component.getComponentName() == "Path") { if (this.previousComponent === null) { this.incomingPaths.push(component); component.setEndPoint(this.xLoc,this.yLoc); component.assignNextComponent(this); } else { alert("Comp. "+this.componentID+" assigned incoming path when previousComponent already defined."); } } else { if (this.incomingPaths.length <= 0) { this.previousComponent = component; } else { alert("Comp. "+this.componentID+" assigned previousComponent when incoming path already defined."); } } }; this.assignOutgoingPath = function(component) { if (component.getComponentName() == "Path") { if (this.nextComponent === null) { this.outgoingPaths.push(component); component.setStartPoint(this.xLoc,this.yLoc); component.assignPreviousComponent(this); } else { alert("Comp. "+this.componentID+" assigned outgoing path when nextComponent already defined."); } } else { if (this.outgoingPaths.length <= 0) { this.nextComponent = component; } else { alert("Comp. "+this.componentID+" assigned nextComponent when outgoing path already defined."); } } }; this.setLocation = function(x,y) { this.xLoc = x; this.yLoc = y; }; this.getLocation = function() { return {x: this.xLoc, y: this.yLoc}; }; /*this.competeForPermission = function(path,proposedArrivalTime) { if ((proposedArrivalTime < this.firstArrivalTime) && (this.permissionID < 0)) { } }; */ this.drawData = function() { this.nodeColor = "#FF00FF"; drawNode(this.xLoc+0.5,this.yLoc+0.5,3,this.nodeColor); } this.isOpen = function() { if (this.outgoingPaths.length > 0) { return this.outgoingPaths[0].isOpen(); } else { return this.nextComponent.isOpen(); } }; this.forwardEntity = function(entity) { //in the future figure out which outgoing path to take //based on routing and shortest path if (this.outgoingPaths.length > 0) { this.outgoingPaths[0].receiveEntity(entity); } else { this.nextComponent.receiveEntity(entity); } }; this.receiveEntity = function(entity) { //only called if the sender knows it's ok to do so this.forwardEntity(entity); }; } //Node component |
Here’s the updated code for the Path components. It is almost untouched from its previous version.
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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
function PathComponent() { setOfComponents.push(this); this.componentID = getNewID(); this.componentName = "Path"; this.entryTime = ""; this.entryEntityID = ""; this.countInQueue = 0; this.exitTime = ""; this.exitEntityID = ""; this.exitResidenceTime = ""; this.x1 = 0.0; this.y1 = 0.0; this.x2 = 0.0; this.y2 = 0.0; this.pathLength = 0.0; this.previousComponent = null; this.nextComponent = null; this.entityQueue = []; this.traverseQueue = []; this.endTraverse = 0.0; this.getComponentID =function() { return this.componentID; }; this.getComponentName =function() { return this.componentName; }; this.getEntryTime =function() { return this.entryTime; }; this.getEntryEntityID =function() { return this.entryEntityID; }; this.getExitTime =function() { return this.exitTime; }; this.getExitEntityID =function() { return this.exitEntityID; }; this.getCountInQueue =function() { return this.countInQueue; }; this.getExitResidenceTime =function() { return this.exitResidenceTime; }; this.setStartPoint = function(x1,y1) { this.x1 = x1; this.y1 = y1; }; this.setEndPoint = function(x2,y2) { this.x2 = x2; this.y2 = y2; }; this.setSpeedTime = function(speed, maxRefreshTime) { this.speed = speed; this.maxRefreshTime = maxRefreshTime; }; this.calcPathLength = function() { this.pathLength = Math.sqrt((this.x2-this.x1)*(this.x2-this.x1) + (this.y2-this.y1)*(this.y2-this.y1)); this.endTraverse = this.pathLength; }; this.isOpen = function() { if (this.entityQueue.length > 0) { if (this.traverseQueue[0] < (10 + 2)) { return false; } } return true; }; this.drawData = function() { //this.nodeColor = "#FF00FF"; this.lineColor = "#FFFFFF"; globalCTX.strokeStyle = this.lineColor; globalCTX.beginPath(); globalCTX.moveTo(this.x1+0.5,this.y1+0.5); globalCTX.lineTo(this.x2+0.5,this.y2+0.5); globalCTX.stroke(); //drawNode(this.x1+0.5,this.y1+0.5,3,this.nodeColor); //drawNode(this.x2+0.5,this.y2+0.5,3,this.nodeColor); var numEntities = this.entityQueue.length; if (numEntities > 0) { for (var i=0; i<numEntities; i++) { this.entityQueue[i].drawEntity(); } } }; this.assignPreviousComponent = function(previous) { this.previousComponent = previous; }; this.assignNextComponent = function(next) { this.nextComponent = next; }; this.backPropogateEndTraverse = function(traverse) { if (this.previousComponent !== null) { } }; this.moveEntity = function(entity) { //moves entities down path, but keeps them from overlapping //for now assumes radius of 5 and clearance of 2 //start from discharge end of list var index = this.entityQueue.length - 1; while ((this.entityQueue[index].entityID != entity.entityID) && (index > 0)) { index--; } var distanceIncrement = this.speed * this.maxRefreshTime; var dist = distanceIncrement; var timeIncrement = this.maxRefreshTime; var nextState = "move"; if (index < this.entityQueue.length - 1) { //not the lead entity, check against next entity if (this.traverseQueue[index]+distanceIncrement > (this.traverseQueue[index+1] - 10 - 2)) { dist = this.traverseQueue[index+1] - 10 - 2 - this.traverseQueue[index]; if (dist <= 0.0) { dist = 0.0; timeIncrement = this.maxRefreshTime; //0.5; } else { timeIncrement *= dist / distanceIncrement; } } } else { //this is the lead entity, check against end of path if (this.traverseQueue[index]+distanceIncrement > this.pathLength) { dist = this.pathLength - this.traverseQueue[index] + 0.001; timeIncrement *= dist / distanceIncrement; nextState = "forward"; } } //TODO: also check against endTraverse, which considers possible entity on subsequent path this.traverseQueue[index] += dist; //distanceIncrement; var frac = this.traverseQueue[index] / this.pathLength; var x = this.x1 + (this.x2 - this.x1) * frac; var y = this.y1 + (this.y2 - this.y1) * frac; entity.setLocation(x,y); feq.newItem(globalSimClock+timeIncrement,this,nextState,entity); displayProgressText("Path comp. "+this.componentID+" moves entity "+entity.entityID+" at time "+globalSimClock.toFixed(6)); }; this.forwardEntity = function() { if (this.nextComponent.isOpen()) { this.traverseQueue.pop(); var entity = this.entityQueue.pop(); if (entity) { this.countInQueue--; this.exitTime = globalSimClock; this.exitEntityID = entity.entityID; displayProgressText("Path comp. "+this.componentID+" forwards entity: "+this.exitEntityID+" at time "+globalSimClock.toFixed(6)); this.nextComponent.receiveEntity(entity); } } }; this.receiveEntity = function(entity) { //receive the entity entity.setLocalEntryTime(); //record time entity entered queue entity.setLocation(this.x1,this.y1); feq.newItem(globalSimClock+this.maxRefreshTime,this,"move",entity); this.traverseQueue.unshift(0.0); this.entityQueue.unshift(entity); this.countInQueue++; //display what was done this.entryTime = globalSimClock; this.entryEntityID = entity.entityID; displayProgressText("Path comp. "+this.componentID+" receives entity: "+this.entryEntityID+" at time "+globalSimClock.toFixed(6)); //if (this.nextComponent.isOpen()) { // this.forwardEntity(); //} }; this.activate = function(nextState,entity2) { if (nextState == "move") { this.moveEntity(entity2); } else if (nextState == "forward") { this.forwardEntity(); } else { errorUndefinedAdvanceState(this.entityID,this.nextState); } }; //this.activate } //PathComponent |
Here’s the new code that initializes everything.
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 |
//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 arrival1 = new ArrivalsComponent(30.0,arrivalSchedule); arrival1.defineDataGroup(10,10,80,"#00FFFF","#FF0000","#FFFF00"); var entry1 = new EntryComponent(2.0); entry1.defineDataGroup(175,10,80,"#00FFFF","#FF0000","#FFFF00"); arrival1.assignNextComponent(entry1); var node1 = new NodeComponent(180,73); var node2 = new NodeComponent(320,134); var node3 = new NodeComponent(180,196); entry1.assignNextComponent(node1); var path1 = new PathComponent(); node1.assignOutgoingPath(path1); node2.assignIncomingPath(path1); path1.setSpeedTime(7.5,1.0); path1.calcPathLength(); var path2 = new PathComponent(); node2.assignOutgoingPath(path2); node3.assignIncomingPath(path2); path2.setSpeedTime(7.5,1.0); path2.calcPathLength(); var exit1 = new ExitComponent(2.0); exit1.defineDataGroup(175,196,80,"#00FFFF","#FF0000","#FFFF00"); node3.assignOutgoingPath(exit1); |
It doesn’t look any different than it did before, and it still works, so that’s always good!