Team Members: Neil Narayan, Chin Yee Lee, and Leonardo Harth
The Problem: Market-Frankford Line System Capacity
SEPTA is currently looking for ways to improve performance on their most heavily used rail line: The Market-Frankford Line. To improve capacity, SEPTA has piloted A/B trains at peak and has started removing seats from trains.
One area that has not been addressed is dwell time, the time that trains spend at the platform to allow passengers to board and alight. Reducing dwell time even by a few seconds per stop can aggregate to significant savings over the course of run, which directly translates into additional system capacity.
Platform Intervention
Our system attacks the dwell time issue at the source while also making some necessary safety improvements to the environment. Currently, there are no warnings for when a train is approaching and disaffected passengers stand around in uninviting spaces. Consequently, passengers tend to cluster in groups around entrances, making it difficult for people to get to the platform. Additionally, people clustering on the platform make it more difficult for both passengers to get off the train and passengers to squeeze on the train. Thus, platform crowding causes dwell time to increase, which in turn, causes train delays and overall reduction in capacity of the system.
Current situation of uneven platform crowding causes dwell time to increase.
Our proposal seeks to achieve dwell time reduction by encouraging people to redistribute themselves on the platform.
Other Interventions / Precedents
Other agencies and companies have also worked on ways to solve this particular problem:
-
London
In London, certain stations are equipped with sign boards to alert passengers where capacity may be available on the train. Unfortunately, these boards are not always visible to every passenger on a platform.
-
New York City
The New York City Subway (MTA) has an app that tells passengers which cars they should board for their most efficient transfer or to get to the exit they need. While this is useful for travelers, it may actually exacerbate the problem if there is a popular connection or exit.
-
Select cities: London, Paris
The City Mapper app has a feature that makes recommendations on the best place to board a train. Unfortunately, the app is only available in select markets and not all markets have this feature. Furthermore, only people with the app have access to this information.
City Mapper
-
Washington, DC
The Washington, DC Metro has lights embedded in the platform at each of their stations and platform announcements are made to alert passengers that a train is arriving. This system is only used as a safety measure to warn passengers to step away from the track area.
Washington, DC Metro
Our Solution: Platy++
Platy++ is an intervention to help ease platform crowding to address the dwell time issue at its source. The system is designed to engage, warn, and inform patrons on the Market-Frankford Line. Strain gauges on the track detect the weight of each train car as it passes over since that is a good proxy for capacity on board. The weight of the train cars will be conveyed to the passengers waiting on the platform to let them know where they should move for optimal boarding.
The lighting response serves three main functions:
Engage: The system uses lights embedded in the platform to engage people to pay attention to their surroundings. Multi-colored lights sweep across the platform encouraging passengers to enjoy and explore the space they are in.
Warn: As sensors down the track pick up the presence of a train, the light show turns to flashing red to signal passengers to step away from the edge of the platform.
Inform: Once the capacity of all cars has been measured, the information is transmitted to the platform where the lights stop blinking, and turn to a gradient from green to red, showing which cars have the most space available.
Below is a system diagram showing how Platy++ works:
The following video shows our prototype of the system in action!
Deployment
To pilot this program, we recommend that Platy++ be installed in the 15th Street station in Center City Philadelphia. Located directly underneath City Hall, the 15th Street Station handles the biggest passenger load on the Market Frankford Line. This site would maximize the visibility of the installation to all users of the Market Frankford Line as well as have the highest impact on dwell time. Because the travel distance between 30th Street Station and 15th Street Station is the longest distance between stops, implementing this system between these stations allows us to have time to read the capacity available in each car, transmit it to the platform, and have passengers safely move to an optimal boarding area.
Furthermore, Platy++ would provide a unique opportunity to have the 15th Street Station match the Dilworth Park, located directly above it. Dilworth Park, the welcome mat to City Hall, just received a $55 million upgrade a few years ago, and yet, the interior of the station is still dilapidated. SEPTA has plans to improve the 15th Street station along with the others, and this solution could be implemented relatively inexpensively while that rehabilitation program is happening.
Dilworth Park currently looks like this:
Whereas, 15th Street Station, directly below is in dire need of an upgrade…
…Which could look like this once Platy++ is installed!
Design Considerations / Future Work
-
Cost feasibility and logistics
When designing Platy++, cost was considered at every step. By embedding the sensors in the track instead of doing on-train solution significantly reduces the cost of both collecting the information and transmitting it. Since SEPTA runs hundreds of trains on the line, each would have to be retrofitted to have capacity sensors and then wireless transmitters and receivers would have had to be installed on the train and at each station.
The on-track solution reduces the number of sensors and allows for the transmission to be hard-wired. The electricity required to power the system is already present adjacent to the rails. Since wireless communication in a tunnel can be unreliable, a hard-wired solution was more desirable for our purposes.
-
Train berthing and stop position
Another early problem we noticed was the lack of consistency in train berthing on SEPTA systems. Unlike modern train systems in other countries, the Market Frankford Line relies on operators to position trains properly at station platforms. Our system will not work properly if train cars do not align to platform lighting. Therefore, we implemented a system to help operators stop more consistently at train platforms. The graphic below illustrates how our berthing system works:
- 1: When the train enters the station, the first overhead ultrasonic sensor will detect its entry. This sends a signal to light up the yellow light, followed by the red light a few seconds later – here, the intention is for the driver to start slowing down upon entry into the station, and stopping at the red light. If implemented, this signaling pattern has to be tuned for the specific timings of trains braking and stopping at different stations.
- 2: When the second sensor detects a train beneath it, the berthing gate will rise up a few seconds later. This means that train drivers will have to stop at this point along the platform, or the berthing gate will not rise to allow it to leave the station.
Prototype
Components:
- Arduino Board [x2]
- Distance Sensor [x2]
- LCD Screen
- LED Lightstrip
- Force Sensor
- Train [Lego Bricks + Jenga Blocks for weight]
Code
Weight Sensor and LED Strip
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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
//SET UP LED STRIP #include <Adafruit_NeoPixel.h> int lightcount = 0; // stores the light value that is being written on each iteration of the code during the light show int countup = 0; // sets the direction that the lights will move across the platform (left to right / right to left) int weightcolor; // a variable that will hold the weight of the car mapped on a 0-255 scale to reflect the way the colors are written int reset; #define RESETPIN 5 //this pin was initially going to be used to reset the state of the board back to the light show instead of using a timer. this feature did not get implemented. #define PIN 6 #define PIN1 6 // ADD IN THE OTHER PINS THE LED STRIPS ARE CONNECTED TO HERE #define PIN2 7 #define PIN3 8 #define PIN4 9 #define NUM_LEDS 30 // the strip has been cut in half - so only 30 lights per strip (no sense adjusting down to 29 yet) #define BRIGHTNESS 50 // THIS MAY NEED TO BE MADE BRIGHTER TO SEE ALL THE LIGHTS ON ALL THE STRIPS #define NUMPIXELS 30 int PXLALL[] = {0,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}; Adafruit_NeoPixel pixels1 = Adafruit_NeoPixel(NUMPIXELS, PIN1, NEO_GRBW + NEO_KHZ800); Adafruit_NeoPixel pixels2 = Adafruit_NeoPixel(NUMPIXELS, PIN2, NEO_GRBW + NEO_KHZ800); Adafruit_NeoPixel pixels3 = Adafruit_NeoPixel(NUMPIXELS, PIN3, NEO_GRBW + NEO_KHZ800); Adafruit_NeoPixel pixels4 = Adafruit_NeoPixel(NUMPIXELS, PIN4, NEO_GRBW + NEO_KHZ800); Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, PIN, NEO_GRBW + NEO_KHZ800); // const int WeightPin = 0; // Analog pin to read in value from force sensor double weight[] = {0,0}; // A variable to store the last weight value and the most recent one const int consist = 4; // The number of cars that make up the train const int axles = consist * 2; // The number of axles per car double Peaks[axles]; // The peak weight of each axle for each car int axlecount = -1; // The count of each axle (the code later requires that we start the count at -1) double carweight[consist]; // Total weight of each car in the train (consist is the technical term for train length) int carcount = -1; // Same as axlecount above - it is set to -1 to save us a step later int CarLights = NUM_LEDS / consist; // how many LEDs should be attributed to each car void setup() { pinMode(RESETPIN, INPUT); // initialize all of the peak values to 0 for (int i = 0; i<axles; i++) { Peaks[i] = 0; } Serial.begin(9600); // Start the Serial Monitor // Print a header line for the output on the Serial Monitor // SERIAL MONITOR OUTPUT: // Current Weight :tab: Axel[0] :tab: Axle[1] ...:tab:... Axle[j] Serial.print("Current Weight\t"); for (int j = 0; j < axles; j++) { Serial.print("Axle["); Serial.print(j); Serial.print("]\t"); } Serial.println(""); // initalize all of the car weights equal to zero. for (int k = 0; k < consist; k++) { carweight[consist] = 0; } pixels1.setBrightness(BRIGHTNESS); // we may need to play with the brightness setting to get all the LED boards to display correctly pixels1.begin(); pixels1.show(); // Initialize all pixels to 'off' pixels2.setBrightness(BRIGHTNESS); // we may need to play with the brightness setting to get all the LED boards to display correctly pixels2.begin(); pixels2.show(); // Initialize all pixels to 'off' pixels3.setBrightness(BRIGHTNESS); // we may need to play with the brightness setting to get all the LED boards to display correctly pixels3.begin(); pixels3.show(); // Initialize all pixels to 'off' pixels4.setBrightness(BRIGHTNESS); // we may need to play with the brightness setting to get all the LED boards to display correctly pixels4.begin(); pixels4.show(); // Initialize all pixels to 'off' } void loop() { int lednum; // this variable counts the number of LEDs to use per car weight[0] = weight[1]; // Shift the weight reading array down by 1 in the array weight[1] = analogRead(WeightPin); // Read in the current value //delay(50); // Write a small delay (50ms) into the sample time // the below code shows it for each individual train car Serial.print(weight[1]); Serial.print("\t"); for(int j = 0; j < axles; j++) { Serial.print(Peaks[j]); Serial.print("\t"); } Serial.println(""); // Using 100 as the "zero" value helps to filter out noise that might act as a false positive for a wheel hit if (weight[1] > 100 && weight[0] <= 100) //start of a wheel hit { LEDBlink(); axlecount++; // increment the count for the next axle (this is why the intial axlecount was -1 Peaks[axlecount] = weight[1]; // the first hit value sets the Peak baseline for that axle } else if (weight[1] > 100 && weight[0] > 100) // the wheel is still rolling over the sensor { LEDBlink(); if(weight[1] > Peaks[axlecount]) //if the recorded weight is higher than any other in the same axle hit - record it as a peak { //Every time a new weight is read in - check to see if it is a new maximum Peaks[axlecount] = weight[1]; } // Serial.print(weight[1]); // Serial.print("\t"); // Serial.println(Peaks[axlecount]); } else if (weight[1] <= 100 && weight[0] > 100) // end of wheel hit { LEDBlink(); if (axlecount == axles-1) // check to see if we need to reset the axles for the next train { // resets all of the variables to their initial state axlecount = -1; for (int k = 0; k < axles; k=k+2) { carweight[k/2] = Peaks[k+1]+Peaks[k]; } // THIS BLOCK OF CODE SHOULD BE REPLACED BY THE FUNCTION THAT SHOWS THE CAR CAPACITY for (int j = 0; j < consist ; j++) { for (int m = 0; m < CarLights; m++) { lednum = m+(j*CarLights); // lednum = m (a counter for the number of lights per car) + (offset) j (the numeber of cars in the train) Serial.print("Car #"); Serial.print(j); Serial.print("\t"); Serial.println(carweight[j]); weightcolor = map(carweight[j],200,2000,0,255); // take the range of weights the cars can fall into and map it to 0-255 pixels1.setPixelColor(lednum,pixels1.Color(0+weightcolor,255-weightcolor,0,0)); pixels2.setPixelColor(lednum,pixels2.Color(0+weightcolor,255-weightcolor,0,0)); pixels3.setPixelColor(lednum,pixels3.Color(0+weightcolor,255-weightcolor,0,0)); pixels4.setPixelColor(lednum,pixels4.Color(0+weightcolor,255-weightcolor,0,0)); } } pixels1.show(); pixels2.show(); pixels3.show(); pixels4.show(); reset = digitalRead(RESETPIN); delay(10000); // THIS LINE OF CODE WILL BE REPLACED BY THE "HIGH" SIGNAL FROM CHIN'S BOARD // END BLOCK OF CODE TO REPLACE // Reset all the peak values back to zero as we wait for the next train ... This also returns us to the LED Light Show state on the next iteration of the loop for (int i = 0; i<axles; i++) { Peaks[i] = 0; } } else // the same train is still rolling over - keep sensing { // DO NOTHING } } else{ //zero state if(Peaks[0] > 0 & Peaks[axles] < 100) // if the train is in process of rolling over - stay blinking { LEDBlink(); } else // no train - random lights { /* this block of code is designed to run the lights up and down. once all of the LEDs on the strip have changed color sequentially, they all move back down the line changing color again */ if(lightcount > 29 && countup == 0) // maxed up - start counting down { countup = 1; //counting down lightcount--; } else if (lightcount < 0 && countup == 1) // minned out - start counting back up { countup = 0; // counting up lightcount++; } else if (countup == 0) { lightcount++; pixels1.setPixelColor(lightcount, pixels1.Color(255,255,0)); pixels2.setPixelColor(lightcount, pixels2.Color(255,140,25)); pixels3.setPixelColor(lightcount, pixels3.Color(255,25,102)); pixels4.setPixelColor(lightcount, pixels4.Color(255,25,217)); } else { lightcount--; pixels1.setPixelColor(lightcount, pixels1.Color(0,85,255)); pixels2.setPixelColor(lightcount, pixels2.Color(0,60,179)); pixels3.setPixelColor(lightcount, pixels3.Color(0,153,25)); pixels4.setPixelColor(lightcount, pixels4.Color(77,153,0)); } pixels1.show(); pixels2.show(); pixels3.show(); pixels4.show(); delay(25); } } } void LEDBlink() // note that this function is written to run in 50ms - the reason for that is to make sure we can still take weight measurements while this is happening { for(uint16_t i=0; i<strip.numPixels(); i++) { pixels1.setPixelColor(i, pixels1.Color(255,0,0,0)); // red pixels2.setPixelColor(i, pixels2.Color(255,0,0,0)); // red pixels3.setPixelColor(i, pixels3.Color(255,0,0,0)); // red pixels4.setPixelColor(i, pixels4.Color(255,0,0,0)); // red } pixels1.show(); pixels2.show(); pixels3.show(); pixels4.show(); delay(25); for(uint16_t i=0; i<strip.numPixels(); i++) { pixels1.setPixelColor(i, pixels1.Color(0,0,0,0)); pixels2.setPixelColor(i, pixels2.Color(0,0,0,0)); pixels3.setPixelColor(i, pixels3.Color(0,0,0,0)); pixels4.setPixelColor(i, pixels4.Color(0,0,0,0)); } pixels1.show(); pixels2.show(); pixels3.show(); pixels4.show(); delay(25); } |
Berthing System and LED Display Screen
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 |
#include <LiquidCrystal.h> // includes the LiquidCrystal Library #include <Servo.h> //includes the Servo library //#include <SoftwareSerial.h> LiquidCrystal lcd(1, 2, 4, 5, 6, 7); // Creates an LCD object. Parameters: (rs, enable, d4, d5, d6, d7) const int trigPin1 = 9; // Attaches pin 9 to the Trigger Pin const int echoPin1 = 10; //Attaches pin 10 to the Echo Pin long duration1; //Set long variable duration as the time between Trigger and Echo int distanceCm1; //Set distance variable const int trigPin2 = 11; // Attaches pin 9 to the Trigger Pin const int echoPin2 = 12; //Attaches pin 10 to the Echo Pin long duration2; //Set long variable duration as the time between Trigger and Echo int distanceCm2; //Set distance variable const int LCDYELLOW = 8; const int LCDRED =3; Servo gateServo; //create servo object to control a servo int pos=0; //variable to store the servo position void setup() { //Serial.begin(9600); lcd.begin(16,2); // Initializes the interface to the LCD screen, and specifies the dimensions (width and height) of the display pinMode(trigPin1, OUTPUT); pinMode(echoPin1, INPUT); pinMode(trigPin2, OUTPUT); pinMode(echoPin2, INPUT); pinMode(LCDYELLOW, OUTPUT); //yellow pinMode(LCDRED, OUTPUT); //red gateServo.attach(13); // Attaches pin 13 to the servo object pinMode(0, OUTPUT); } void loop() { digitalWrite(trigPin2, LOW); //Send out a low pulse from Trigger Pin delayMicroseconds(2); //A sharp delay of 2 microseconds before... digitalWrite(trigPin2, HIGH); //...sending out a high pulse from the Trigger Pin delayMicroseconds(10); //A longer delay of 10 microseconds before... digitalWrite(trigPin2, LOW); //...sending out a low pulse again. duration2 = pulseIn(echoPin2, HIGH); //duration is defined as the time at which the echopin detects a high pulse distanceCm2= duration2*0.034/2; //distance calculation based on known speed of pulse wave and the detected time //Serial.println(distanceCm2); if (distanceCm2<10.50) { digitalWrite(LCDYELLOW, OUTPUT); digitalWrite(LCDRED, LOW); delay(1000); digitalWrite(LCDYELLOW, LOW); digitalWrite(LCDRED, OUTPUT); delay(5000); } else { digitalWrite(LCDYELLOW, LOW); digitalWrite(LCDRED, LOW); } digitalWrite(trigPin1, LOW); //Send out a low pulse from Trigger Pin delayMicroseconds(2); //A sharp delay of 2 microseconds before... digitalWrite(trigPin1, HIGH); //...sending out a high pulse from the Trigger Pin delayMicroseconds(10); //A longer delay of 10 microseconds before... digitalWrite(trigPin1, LOW); //...sending out a low pulse again. duration1 = pulseIn(echoPin1, HIGH); //duration is defined as the time at which the echopin detects a high pulse distanceCm1= duration1*0.034/2; //distance calculation based on known speed of pulse wave and the detected time lcd.setCursor(0,0); // Sets the location at which subsequent text written to the LCD will be displayed lcd.print("Distance: "); // Prints string "Distance" on the LCD lcd.print(distanceCm1); // Prints the distance value from the sensor lcd.print(" cm"); delay(10); lcd.setCursor(0,1); delay(10); gateServo.write(pos); //set the servo to the position - on the first run it initializes to 0 if (distanceCm1<10.50) //which means an object is detected above the Ultrasound Sensor { lcd.print("Gate is opening!"); //first describe the Gate action if (pos <90) //if the Servo is not already upright { delay(1500); //delay 1.5 seconds before reacting - this mimics the lagged response of toll gates in reality (for safety purposes). for(pos=0; pos<=90; pos++) //gradually moves through 90 degrees { gateServo.write(pos); delay(10); } } else //Servo is already upright - this accounts for when the train remains stationary before the gate { pos=90; //maintain the upright position delay(10); } } else //no object is detected above the Ultrasound Sensor { lcd.print("Gate is closed."); if (pos >0) //Gate is upright and needs to close { delay(1500); //delay 1.5 seconds before reacting for(pos=90; pos>=0; pos--) //gradually moves through 90 degrees { gateServo.write(pos); delay(10); } } else //Gate is already down { pos=0; //remain down delay(10); } } } |
We hope this has been an interesting read! If you are interested to find out more about how parts of this system could be constructed for smaller projects, do check out our tutorials on LED lighting display, weight detection, and toll gate design on this blog as well.