Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lab 583 support vehicles #58

Merged
merged 61 commits into from
Mar 15, 2021
Merged

Lab 583 support vehicles #58

merged 61 commits into from
Mar 15, 2021

Conversation

KasiaKoz
Copy link
Collaborator

  • adds vehicles and vehicle_types attribute to Schedule object.
    • vehicles is a mapping between vehicle IDs stored within Routes and their types so for a bus route with a trip which uses vehicle 'veh_1', there will be an entry 'veh_1': {'type': 'bus'}. Types need not be exactly the same as mode of the route but are as default if generated by genet.
    • vehicle_types is a dictionary holding vehicle definitions which used to be hard coded here. Now you can pass a vehicle definitions config when instantiating a schedule. If not passed, it defaults to that example config (so no change in behaviour, but gives flexibility to change definitions for your use case and also modify them when the Schedule already exists.
  • reads vehicle IDs in MATSim schedule.xml file and the vehicles.xml (which forms vehicles and vehicle_typesattributes) if passed
  • changes route.trips attribute to hold vehicles next to trips. Before
{'trip_1': '07:00:00', ...}

After

{'trip_id': ['trip_1', 'trip_2', ..], 'trip_departure_time': ['07:00:00', '08:00:00', ...], 'vehicle_id': ['veh_1', 'veh_2', ...]}

which also makes it look more sensible in route data summary method. From

n.schedule.route_attribute_summary(data=True)

attribute
├── route_short_name: ['N20', '134', 'N55', '98', 'N5']
├── mode: ['bus']
├── trips
│   ├── VJ2cdccea96e0e3e6a53a968bcb132941415d6d7c9_04:53:00: ['04:53:00']
│   ├── VJ375a660d47a2aa570aa20a8568012da8497ffecf_03:53:00: ['03:53:00']
│   ... massive list, all trips show up
│   └── VJf8c00ef586e4fbf1e7c75d216ed830d039b6e04a_04:23:36: ['04:23:36']

to

n.schedule.route_attribute_summary(data=True)

attribute
├── route_short_name: ['94', '113', 'N8', 'N20', 'N5']
├── mode: ['bus']
├── trips
│   ├── trip_id: ['VJ33e5546bdd3e9281dc8dbe530d67dd7e34352b61_15:09:00', 'VJ7d40809e4e187795fcb14f1befa17036baedb04c_17:32:00', 'VJ093350c7a24291d42b83678a7f298e6c6c875e9f_08:51:00', 'VJe3087fc34c73052fcfbbf9e3256adf7f629f38a9_24:01:00', 'VJ6cc11fb46ab954b63161bf1bee99b178603e3bb7_16:07:00']
│   ├── trip_departure_time: ['09:58:13', '18:06:00', '02:44:00', '09:46:34', '11:07:34']
│   └── vehicle_id: ['veh_1853_bus', 'veh_1715_bus', 'veh_765_bus', 'veh_926_bus', 'veh_529_bus']
  • adds a couple of cool methods
    • route_trips_to_dataframe which generates a DataFrame holding all the trips, their departure times and vehicle IDs, next to the route ID and service ID. ---!! I need help with naming this method, there is another one generate_trips_dataframe which extends this further, expanding across all stops (for all trips) in the route and giving scheduled timestamps at those stops for all trips for all routes, I think they sound too similar and I'm not sure they convey what the outputs are very well >_>
    • set_route_trips_dataframe which takes the output of route_trips_to_dataframe and changes route.trips attributes of all routes present in the DataFrame to what is in the passed DataFrame. This means you can pull out a DataFrame representation of all trips in the Schedule, change their times, IDs or vehicles (so maybe have some vehicles shared based on some logic) and then update the Schedule with those changes just using that DataFrame representation. Example from a jupyter notebook:

Screenshot 2021-02-24 at 09 28 34

Screenshot 2021-02-24 at 09 28 45

be due to missing suitable attribute change methods, which are coming
next
applying functions and dictionary maps to iterator objects of the same
shape
# Conflicts:
#	genet/core.py
#	tests/test_core_schedule_elements.py
or service, test the option to pass schedule.stop('id') for this
purpose too
# Conflicts:
#	genet/exceptions.py
#	genet/outputs_handler/matsim_xml_writer.py
#	genet/schedule_elements.py
#	genet/utils/persistence.py
#	genet/variables.py
#	tests/test_core_components_route.py
#	tests/test_core_components_service.py
#	tests/test_core_schedule.py
#	tests/test_core_schedule_elements.py
#	tests/test_utils_persistence.py
# Conflicts:
#	genet/exceptions.py
#	genet/outputs_handler/matsim_xml_writer.py
#	genet/schedule_elements.py
#	genet/utils/persistence.py
#	genet/variables.py
#	tests/test_core_components_route.py
#	tests/test_core_components_service.py
#	tests/test_core_schedule.py
#	tests/test_core_schedule_elements.py
#	tests/test_utils_persistence.py
@KasiaKoz KasiaKoz requested review from mfitz and gac55 February 24, 2021 09:34
Copy link
Contributor

@gac55 gac55 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slick. Hard to imagine making these kind of changes without good tests now ;)

@@ -0,0 +1,155 @@
# form basis for vehicle modes in the schedule, used when reading in GTFS and present in the schedule
# unless replaced by reading vehicles.xml file
VEHICLE_TYPES:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much easier to read

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also much easier to edit! 👍

# check mode consistency
vehicles_to_modes = df.groupby('vehicle_id').apply(lambda x: list(x['type'].unique()))
if (vehicles_to_modes.str.len() > 1).any():
# there are vehicles which are shared across routes with different modes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this ever happen? Would hope not but good to have a catch for it.
If it does with our ITO data it is something we should feed back to them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so when reading GTFS I give unique vehicles to each trip so this doesn't happen, is there data in the GTFS that could serve as a vehicle? I added this because

  1. people might like to generate vehicles that are shared between trips to make things more realistic, and it's really easily done with the route_trips_to_dataframe and set_route_trips_dataframe methods
  2. People could get their vehicle ids muddled when adding routes and services

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it

Copy link
Contributor

@mfitz mfitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Added some minor comments.

...route_trips_to_dataframe which generates a DataFrame holding all the trips, their departure times and vehicle IDs, next to the route ID and service ID. ---!! I need help with naming this method, there is another one generate_trips_dataframe which extends this further, expanding across all stops (for all trips) in the route and giving scheduled timestamps at those stops for all trips for all routes, I think they sound too similar and I'm not sure they convey what the outputs are very well >_>

How about route_trips_to_dataframe and route_trips_with_stops_to_dataframe? Or route_trips_to_summary_dataframe and route_trips_to__full_dataframe?

@@ -0,0 +1,155 @@
# form basis for vehicle modes in the schedule, used when reading in GTFS and present in the schedule
# unless replaced by reading vehicles.xml file
VEHICLE_TYPES:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also much easier to edit! 👍

@@ -60,3 +60,10 @@ class StopIndexError(Exception):
Raised in case of Stop indexing inconsistency
"""
pass


class InconsistentVehicleModeError(Exception):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brownie_points += 100

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hooked now

@@ -219,9 +220,15 @@ def write_transitLinesTransitRoute(transitLine, transitRoutes, transportMode):

route = [r_val['link']['refId'] for r_val in transitRoute_val['links']]

trips = {}
trips = {
'trip_id': [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a one-to-one relationship between trip IDs, trip departure times, and vehicle IDs? Or will these 3 lists each be of different sizes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's one to one, so at index x in each list there should be a time and vehicle corresponding to that trip_id. It would make sense to make a Trip class and just have a list here but the nestedness of classes is destroying me 😭 Schedule(Service(Route(Stop))) is about as much as I can handle. That's partly why I introduced the dataframe setting method. Thinking about it now, it might be good to have something like this when instantiating a Route, instead of giving the trips dictionary that is exactly matching those assumptions, you can just give a dataframe with those columns, that would enforce equal length of those lists and would be more user friendly I think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Trip class idea was why I was asking this question. I don't think it needs to sit inside a class hierarchy though. You could use a python dataclass, maybe, if the class is really just a way to hold data and won't define any logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh, first time I'm seeing this, I like! I'll add a jira card to that effect, thanks!

read_capacity = False
elif tag == 'capacity':
read_capacity = True
elif read_capacity:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this belong in the elif? Wouldn't it work as just an if?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took me a while to clock this 😂 , the capacity attributes get read before the capacity tag appears, the flag read_capacity will change to True, and then the vehicle type dictionary v will get its capacity set as an empty dictionary (the value of the capacity elem.attrib)

@@ -98,115 +98,3 @@
# 'freespeedFactor': 1.0,
'capacity': 9999.0}
}


MODE_DICT = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete is the best refactoring 😄


def test_generating_vehicles_with_shared_vehicles_and_consistent_modes(mocker, schedule):
schedule.vehicles = {}
mocker.patch.object(DataFrame, 'drop',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this test because it's readily apparent from reading the test alone why we should expect the generated vehicles map to look this way.

'v_3': {'type': 'rail'}})


def test_generating_additional_vehicles_by_default(schedule):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we testing two things here? The test name suggests we're going to assert that an additional vehicle has been generated, but the assertion also seems to be checking that the modes have changed for the pre-existing vehicles. If so, these might be better in separate tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, it's checking that all the others haven't changed, they are all the weird _bus type

'veh_1_bus': {'type': 'bus'}, 'veh_2_bus': {'type': 'bus'}})


def test_generating_vehicles_with_shared_vehicles_and_inconsistent_modes(mocker, schedule):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better test name might be something like test_rejects_inconsistent_modes_when_generating_vehicles

Test names like does_something_when_something can give a better clue about the intent of the test and the expected behaviour of the code under test.

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>

<vehicleDefinitions xmlns="http://www.matsim.org/files/dtd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.matsim.org/files/dtd http://www.matsim.org/files/dtd/vehicleDefinitions_v1.0.xsd">
<vehicleType id="Bus">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, MATSim XML schemas and case...

@@ -33,6 +33,19 @@ def test_swallows_exceptions_making_new_directories(mocker):
os.makedirs.assert_called_once()


def test_is_yml_identifies_yml():
zip_dir = os.path.join('path', 'to', 'dir', 'file.yml')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zip_dir feels like a misleading name. Although you don't really need the variable:

assert persistence.is_yml(os.path.join('path', 'to', 'dir', 'file.yml'))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy pasta fail, typical >_>'

@KasiaKoz KasiaKoz merged commit b625164 into master Mar 15, 2021
@KasiaKoz KasiaKoz deleted the LAB-583-support-vehicles branch March 15, 2021 11:42
@KasiaKoz KasiaKoz mentioned this pull request Apr 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants