A Travel Planer package
This assignment asks you to refactor existing code and package it in a form that can be tested, installed and accessed by other users.
The code to solve most of the problems is already given, but as roughly sketched out code in this document.
Your job consists in converting the code into a formally structured package, with unit tests, docstrings, doctests, a command line interface, and demonstrating your ability to use git version control, GitHub and Travis CI.
The exercise will be semi-automatically marked, so it is very important that you adhere in your solution to the correct file and folder name convention and structure, as defined in the rubric below. An otherwise valid solution which doesnt work with our marking tool will not be given credit.
First, we set out the problem we are solving, and its informal solution. Next, we specify in detail the target for your tidy solution. Finally, to assist you in creating a good solution, we state the marks scheme we will use.
1 The Travel Planner
The code we will be working with generates timetables for bus routes and calculates travel paths and times for a set of passengers.
Each route is provided as a set of coordinates on a grid and a stop identifier for each point, if there is a stop there (e.g., (3, 5, A) indicates that stop A is at x=3, y=5). This is a route weve generated to get you started:
Similarly, we can have a list of people with the coordinates of their origin and their desired destination. Each passenger also has a different pace (how fast they can walk in minutes per unit of the grid).
The first function we have gives us the timetable for a given route. At the moment it assumes a starting time at 0 and a constant speed for the buses (10 minutes per step), however we would eventually like to make it more general.
route=[(2,1,A),(3,1,), (4,1,), (5,1,), (6,1,), (7,1,B),(7,2,), (8,2,), ( 9, 2, ), (10, 2, ), (11, 2, C), (11, 1, ), (12, 1, ), (13, 1, ), (14, 1, ), (14, 2, D), (14, 3, ), (14, 4, ), (13, 4, ), (12, 4, ), (11, 4, ), (10, 4, ), ( 9, 4, ), ( 9, 5, E), ( 9, 6, ), (10, 6, ), (11, 6, F), (12, 6, ), (13, 6, ), (14, 6, ), (15, 6, ), (16, 6, G) ]
passengers = { 1: [(0,2),(8,1), 15],
2: [(0,0),(6,2), 12],
3: [(5,2), (15,4), 16],
4: [(4,5),(9,7), 20],
}
1
def timetable(route):
Generates a timetable for a route as minutes from its first stop.
time = 0
stops = {}
for step in route: if step[2]:
stops[step[2]] = time
time += 10
return stops
In this city, passengers can walk in straight lines from one point to another. Therefore, they can walk from one point to another without worrying about the grid. A passengesr walking distance to the closest starting and ending stop is then calculated as:
def passenger_trip(passenger, route):
start, end, pace = passenger
stops = [value for value in route if value[2]] # calculate closer stops
## to start
distances = [(math.sqrt((x start[0])**2 +
(y start[1])**2), stop) for x,y,stop in stops] closer_start = min(distances)
## to end
distances = [(math.sqrt((x end[0])**2 +
(y end[1])**2), stop) for x,y,stop in stops]
closer_end = min(distances) return (closer_start, closer_end)
Think, however, what happens when two stops are at the same distance! Or when the order of the closest stops does not match the direction of travel!
The total travel time for a particular passenger can be obtained using the following function:
Although that function gives you the total travel time, you probably would prefer to know how much of that time is by foot and how much is on the bus.
Finally, we also have some visualisation tools. One displays the route and the stops on a grid:
def passenger_trip_time(passenger, route): walk_distance_stops = passenger_trip(passenger, route) bus_times = timetable(route)
bus_travel = bus_times[walk_distance_stops[1][1]]
bus_times[walk_distance_stops[0][1]]
walk_travel = walk_distance_stops[0][0] * passenger[2] +
walk_distance_stops[1][0] * passenger[2] return bus_travel + walk_travel
def plot_map(route):
max_x = max([n[0] for n in route]) + 5 # adds padding max_y = max([n[1] for n in route]) + 5
grid = np.zeros((max_y, max_x))
for x,y,stop in route:
grid[y, x] = 1 if stop:
grid[y, x] += 1
fig, ax = plt.subplots(1, 1)
ax.pcolor(grid)
ax.invert_yaxis()
2
ax.set_aspect(equal, datalim)
plt.show()
Another function displays the number of people on the bus between stops:
def plot_bus_load(route, passengers):
stops = {step[2]:0 for step in route if step[2]} for passenger in passengers.values():
trip = passenger_trip(passenger, route)
stops[trip[0][1]] += 1
stops[trip[1][1]] -= 1
for i, stop in enumerate(stops): if i > 0:
stops[stop] += stops[prev]
prev = stop
fig, ax = plt.subplots()
ax.step(range(len(stops)), list(stops.values()), where=post)
ax.set_xticks(range(len(stops)))
ax.set_xticklabels(list(stops.keys()))
plt.show()
With all these functions we can then generate some output with:
print( Stops: minutes from start
, timetable(route)) for passenger_id, passenger in passengers.items():
print(fTrip for passenger: {passenger_id})
start, end = passenger_trip(passenger, route)
total_time = passenger_trip_time(passenger, route)
print((f Walk {start[0]:3.2f} units to stop {start[1]},
f get on the bus and alite at stop {end[1]} and
f walk {end[0]:3.2f} units to your destination.))
print(f Total time of travel: {total_time:03.2f} minutes)
# Plots the route of the bus
plot_map(route)
# Plots the number of passenger on the bus
plot_bus_load(route, passengers)
Stops: minutes from start
{A: 0, B: 50, C: 100, D: 150, E: 230, F: 260, G: 310}
Trip for passenger: 1
Walk 2.24 units to stop A,
get on the bus and alite at stop B and
walk 1.00 units to your destination.
Total time of travel: 98.54 minutes
Busses cant move diagonally, only horizontally or vertically. To check for that, you can convert the route into a chain code and find whether there are odd numbers on it. A chain code is a compressed way to represent data in image processing. Heres a simple implementation of it that you may find useful when checking whether a bus route is valid or not.
def route_cc(route):
Converts a set of route into a Freeman chain code 321
|/ 4-C-0 /| 567
3
start = route[0][:2]
cc = []
freeman_cc2coord = {0: (1, 0),
1: (1, -1),
2: (0, -1),
3: (-1, -1),
4: (-1, 0),
5: (-1, 1),
6: (0, 1),
7: (1, 1)}
freeman_coord2cc = {val: key for key,val in freeman_cc2coord.items()} for b, a in zip(route[1:], route):
x_step = b[0] a[0]
y_step = b[1] a[1]
cc.append(str(freeman_coord2cc[(x_step, y_step)]))
return start, .join(cc)
and this is how to use it:
2 Your Assignment
You are required to make various changes to the code, including adding functionality, changing the structure and creating tests. These are described in more detail below. You can tackle these in any order.
2.1 Reading CSV files
The code you have been provided with does not include any way of reading data. Add a function called read_passengers which will take the name of a file and extract the passenger data contained therein, according to the format described above. Each passenger should be represented as a tuple with three fields, corresponding to their start point, end point, and speed (in that order).
For example, for a file passengers.csv
calling read_passengers(passengers.csv) should return the list: [((0,2), (8,1), 15), ((0,0), (6,2), 12)]
You may also want to create a function for reading routes from a file, again according to the above description whether you do so and what you call it is up to you.
To help you generate random routes and passengers we have created a web service. Find information on how to use it at https://ucl-rse-with-python.herokuapp.com/travel-planner/.
2.2 Restructuring to object-oriented style
Instead of a collection of functions, create three classes Passenger, Route and Journey. The first two will represent a single passenger and a route for a bus. The third one will represent a particular journey
start_point, cc = route_cc(route)
print((fThe bus route starts at {start_point} and
fits described by this chain code:
{cc}))
The bus route starts at (2, 1) and
its described by this chain code:
0000060000200066644444660000000
0, 2, 8, 1, 15
0, 0, 6, 2, 12
4
along a route for a specific collection of passengers, and will keep track of how long it takes, how many passengers are on board at any time, etc. How the classes are implemented internally is up to you, but they must provide the functionality described below.
Here is an example of using the new classes:
The Passenger class should have the following methods:
constructor: takes a starting point, end point and speed
walk_time(): gives the time that this passenger would need to get from their starting point to their
end point, walking at their speed.
The Route class should have the following methods:
constructor: takes a file name
plot_map(): plots and displays a map of the route
timetable(): returns a dictionary mapping stop names to times, assuming the first stop is at time
0.
generate_cc(): returns a tuple containing the starting point and a string with the chain code.
The Journey class should have the following methods:
constructor: takes a Route and a list of Passenger objects
plot_bus_load(): plots and displays a graph of the number of passengers on the bus as it moves
along the route
travel_time(passenger_id) and print_time_stats(): see next section
With the exception of the travel_time and print_time_stats methods, the other functionality is already included in the provided code, although you may need to make some changes as you move to the object-oriented structure.
2.3 Adding new functionality to journeys
The Journey class should include two methods providing new behaviour which does not exist in the original code. A Journey object will keep track of the passengers that are present, including how long they had to walk to or from the bus to complete their journey.
The travel_time method should take as input the id of a passenger; this is the order in which the passenger appears in that Journeys constructor. For instance, in the example:
then Mary is assigned an id 0, and John an id of 1.
Calling travel_time(i) should return a dictionary of times for the passenger with the specified id. This dictionary will have the keys bus and walk, and the corresponding values will be how long that passenger spent on that mode of transport. Remember that passengers should choose to walk if that is faster than taking the bus, so either time could potentially be 0! Note that in the case of two stops being at the
route = Route(route.csv)
john = Passenger(start=(0,2), end=(8,1), speed=15)
passengers = [
Passenger(start, end, speed) for start, end, speed
in read_passengers(filename)
]
journey = Journey(route, passengers)
journey.plot_bus_load()
john = Passenger(start=(0,2), end=(8,1), speed=15)
mary = Passenger(start=(0,0), end=(6,2), speed=12)
route = Route(my_route.csv)
journey = Journey(route, [mary, john])
5
same walking distance for a particular passenger from their origin or destination, the travel suggested should select the bus stops that minimise the total travel time (i.e., the latter stop to get on the bus and the earlier to alite).
The other new method is print_time_stats. This should not return anything, but should print the following statistics:
where xxxx is the average bus/walking time over all passengers on this Journey.
Note that both these methods can be based on existing code samples provided in this description, but you
will need to add to them to achieve the correct functionality.
2.4 Adding flexibility and validation
Further to the above additions, you should make the code more flexible and robust in two ways.
First, we want to be able to specify the speed of the bus when creating a route. If not specified, assume that a bus has a speed of 10 minutes per step.
Secondly, when creating a route from a file, we want to check that the route is valid, i.e. that the bus only moves horizontally or vertically. If the given route has any diagonal movements, the code should throw an error.
2.5 Testing locally and on Travis
You should create tests that check whether the code is behaving correctly. These should cover all three classes and their methods. Remember to include negative tests, such as checking for an error if the input is invalid.
Additionally, you should set up the Travis Continuous Integration server for your repository. This will allow you to run tests whenever you push code to your GitHub repository. It requires adding a special YAML file with instructions. For help with this, the solution to the packaging classwork exercise includes an example of a Travis configuration file, and the exercise itself has a link to more information about Travis.
2.6 Packaging
You should put your code in a Python package that lets others install and use it. The package should be called travelplanner, so the following code example should work:
The package should include three additional metadata files, describing how to cite it, under what conditions it can be used, and instructions for how to use it.
Installing the package should also create a command line interface that can be accessed through the command bussimula. The command is called as follows:
bussimula routefile passfile speed 5 [saveplots]
When called, it should print the same information as the generated output from the example in the intro- duction (timetable, travel indications for each passenger).
The speed option accepts a number indicating the speed of the bus. If this option is not given, the speed should be set to 10 by default.
Average time on bus: xxxx min
Average walking time: xxxx min
from travelplanner import Passenger
john = Passenger(start=(10,15), end=(20,20), speed=15)
6
The saveplots argument is also optional. If given, two plots should also be generated, showing the map of the route and the evolution of the load. These should be saved in files named map.png and load.png respectively.
3 Way of working
Part of this assignment is concerned with the use of version control.
You should create a Git repository for your code, and use git throughout your work. You should also create a repository on GitHub to hold your code (more details about this, below). When making changes, focus on small pieces of work and follow this workflow:
1. Open a GitHub issue describing the problem or desired improvement.
2. Create a new branch which will hold the code for addressing the above.
3. Open a Pull Request (PR) on GitHub from the new branch to master.
4. Make sure that the tests are passing (including on Travis, if set up) before merging the PR.
As part of your submission, we will ask you to show evidence that you have been following these steps.
3.1 Directory structure
You must submit your exercise solution to Moodle as a single uploaded gzip format archive. (You must use only the tar.gz, not any other archiver, such as .zip or .rar. If we cannot extract the files from the submitted file with gzip, you will receive zero marks.)
To create a tar.gz file you need to run the following command on a bash terminal:
tar zcvf filename.tar.gz directoryName
The folder structure inside your tar.gz archive must have a single top-level folder, whose folder name is your
student number, so that on running
tar zxvf filename.tar.gz
this folder appears. This top level folder must contain all the parts of your solution (the repository and misc). You will lose marks if, on extracting, your archive creates other files or folders at the same level as this folder, as we will be extracting all the assignments in the same place on our computers when we mark them!
Inside your top level folder, you should have a repository and a misc directory. Only the repository directory has to be a git repository. Within the repository directory, create a setup.py file to make the code installable. You should also create some other files, per the lectures, that should be present in all research software packages. (Hint, there are three of these.) The misc directory should not be version controlled and contain a reasoning of why a particular license was chosen and two screenshots, one of the issues youve created on GitHub and another of the pull-requests. To get on the same page open and close issues go to the issues page of your repository and filter with only: is:issue (similarly for pull-requests).
Your tidied-up version of the solution code should be in a sub-folder called travelplanner which will be the python package itself. It will contain an __init__.py file, and the code itself must be in one or multiple files. They should define a class Route, a class Passenger and a class Journey; instead of a data structure and associated functions, you must refactor this into a class and methods.
Thus, if you run python in your top-level folder, you should be able to do
from travelplanner import Route, Passenger, Journey
If you cannot do this, you will receive zero marks.
7
You must create a command-line entry point, called bussimula. This should use the entry_points facility in setup.py, to point toward a module designed for use as the entry point, in travelplanner/command.py. This should use the argparse library to implement the behaviour explained above.
You must create unit tests which cover a number of examples. These should be defined in travelplan- ner/tests/test_
from ..route import Route
If your unit tests use a fixture file to DRY up tests, this must be called travelplanner/tests/fixtures.yml. For example, this could contain a yaml array of many routes setups. Remember to also include at least a negative test.
You should git init inside your repository folder, as soon as you create it, and git commit your work regularly as the exercise progresses. Due to our automated marking tool, only work that has a valid git repository, and follows the folder and file structure described above, will receive credit.
Due to the need to avoid plagiarism, do not use a public github repository for your work instead, use the repository created by following this link: https://classroom.github.com/a/lFCbof8c which after you ac- cept the permissions will create a repository named mph0021-2019-travel-planner-
In summary, your directory stucture as extracted from the studentNumber.tar.gz file should look like this:
studentNumber/
repository/
.git/
travelplanner/
tests
__init__.py
file_a.py
__init__.py
test_file_a.py fixture_data.yaml
setup.py
file_1.md
file_2.md
file_3.md
misc/
issues.png
pull_requests.png
license_reasoning.md
Remember to no include artefacts files (like for example: .pyc, .DS_Store) to your submission, their presence will remove points to your final mark.
8
4 Mark Scheme
Note that because of our automated marking tool, a solution which does not match the standard solution structure defined above, with file and folder names exactly as stated, may not receive marks, even if the solution is otherwise good. Follow on marks are not guaranteed in this case.
Version control with git and github ussage (8 marks)
Senssible commit sizes (1 mark)
Appropriate commit messags (1 mark)
Generate issues on GitHub (submit as a snapshot of your repository) (1 mark)
Use branches and pull requests for each feature (up to 3 mark, one per feature)
Use of Travis-CI and only merge with passing jobs (2 mark) Note A passing job doesnt need
tests, if you start creating a travis job with the provided script the PR can check that it runs.
You should start to run the tests when you create them. Code structure (6 marks)
One file for the commands to execute as entry point (1 mark)
One or more files for the classes definition for Route, Passenger and Journey (1 mark) Defines the classes with a valid object-oriented structure (2 marks)
Allows setting the bus speed (1 mark)
Throws an error when input an invalid route (Hint with diagonal moves) (1 mark)
Command line entry point (7 marks)
Which correctly uses the argparse library (2 mark)
Accepting a route and passenger definition text files as inputs (1 marks)
Which prints the results (bus stop times and travel indications and time for each passenger) to
standard out (1 mark)
With an optional parameter to set the average speed of the bus (1 mark)
With an optional parameter to save the plots of the map and the bus load in files (2 marks)
Testing (15 marks)
At least one test for each of the following methods (walk_time, timetable, travel_time,
print_time_stats, generate_cc, and each constructor) (8 marks)
Which use a fixture file or other approach to avoid overly repetitive test code (1 mark)
Which at least one test that checks how the code fails when invoked incorrectly (1 mark)
At least one doctest (1 mark)
Tests and doctests running on Travis (2 marks)
Coverage up to 80% (2 mark * coverage/80)
Style (3 marks)
Good variable, functions and classes names (1 mark) Follows pep8 style on all the files (2 marks)
Packaging (11 marks)
setup.py file which could be used to pip install the project (1 mark)
With appropriate metadata, including version number and author (1 mark)
Which packages code (but not tests), correctly. (1 mark)
Which specifies library dependencies (1 mark)
Which points to the entry point function (1 mark)
Which allows to import Passenger, Route, Journey from the library (1 mark)
from travelplanner import Passenger, Route, Journey
Three other metadata files: (5 marks) Hint: How to use the package (2), how to reference it (1), who can copy it (2, one for the file and one for a reasoning for the choice).
9
Reviews
There are no reviews yet.