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

Add table of per-edge factors to network when modification requires it #838

Merged
merged 6 commits into from
Dec 20, 2022

Conversation

abyrd
Copy link
Member

@abyrd abyrd commented Nov 17, 2022

Some of the options for ModifyStreets modifications adjust generalized costs for walking and biking. These options would only work on special networks containing a special generalized costs data table that is not present by default in most networks. Those data tables are currently only created in networks built from very particular OSM data where every way has all of the special tags contributing to the LADOT generalized cost (com.conveyal.r5.streets.LaDotCostTags).

Some users have tried out the walk cost and bike cost ModifyStreets parameters and seen cryptic messages like "bikeGenCostFactor can only be set to values other than 1 on networks that support per-edge factors." Rather than keeping and repeatedly explaining this disappointing/confusing behavior, I decided to change the modification apply() code to simply create the data table in the scenario copy of the network as needed. This means each scenario will have its own (potentially large) generalized cost data table instead of just extending a shared one in the baseline network. This is a rarely used specialty modification, so such less-than-optimal implementation is acceptable at least as a stopgap. The other alternatives would be:

  1. Add the table to the baseline network whenever it's any scenario needs to extend it. This breaks a lot of conventions we have about treating loaded networks as read-only, and incurs a lot of extra memory access and pointless multiplication by 1 on every scenario including the baseline.
  2. Require the table to be enabled on the base network when it's first built, using a parameter in TransportNetworkConfig. This incurs the same overhead, but respects the immutable character of loaded networks and is an intentional choice by the user.

This works as expected in my own testing.

abyrd and others added 3 commits November 17, 2022 00:39
this is currently modifying the scenario network in the resolve phase
which is probably bad form - ideally we'd shift this to the apply phase.
switch to factory method for EdgeTraversalTimes
Create EdgeTraversalTimes in apply phase instead of resolve phase
change info message grammar to work with "Modification X" as a header
Copy link
Member

@trevorgerhardt trevorgerhardt left a comment

Choose a reason for hiding this comment

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

This looks good overall, and I agree with the premise.

Is unity descriptive enough? I am less familiar with this area of work, so I may not have the proper context, but method names of
EdgeTraversalTimes.generateNeutralEdges(...) and setAllEdgesToNeutral() might be more appropriate for what they end up doing.

Comment on lines 118 to 119
times.bikeTraversalTimes.setAllUnity();
times.walkTraversalTimes.setAllUnity();
Copy link
Member

Choose a reason for hiding this comment

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

Additionally, this section could be replaced with

Suggested change
times.bikeTraversalTimes.setAllUnity();
times.walkTraversalTimes.setAllUnity();
for (int i = 0; i < edgeStore.nEdges(); i++) {
times.bikeTraversalTimes.setOneEdge();
times.walkTraversalTimes.setOneEdge();
}

and the new setAllUnity method could be removed.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I just had a reflex of aiming for better encapsulation ("tell the object for each mode to set its own internal fields") but the EdgeTraversalTimes and SingleModeTraversalTimes are already essentially friend classes. If it's easier to understand without the extra method I'll remove it. I'll rename the existing method though because "setOneEdge" should be something like "addOneNeutralEdge".

@abyrd
Copy link
Member Author

abyrd commented Nov 22, 2022

Is unity descriptive enough?

I guess this depends on perspective. By "unity" I meant an element that behaves like the number 1, emphasizing that some item scales another item to the same size it already is, rather than being a specific quantity. Applying elements of the type EdgeTraversalTimes to elements of the type EdgeStore effectively transforms the EdgeStore to one with different perceived travel times. There's one specific element of EdgeTraversalTimes that has no effect on any EdgeStore, which is the identity element. I almost used the name "identity" for that reason but "identity" seemed too ambiguous. I guess "unity" is not any better. According to https://en.wikipedia.org/wiki/Identity_element it looks like "neutral element" is a synonym so yes, "neutral" might be more intuitive. Some description of how types behave in terms of algebra can help reason about code and would be helpful for some readers, but that can just be a note in the Javadoc.

use "neutral" instead of "unity"
eliminate nested function calls to improve clarity
reuse existing methods to extend with neutral per-edge factors
@abyrd
Copy link
Member Author

abyrd commented Nov 28, 2022

Changes have been made to names and function call nesting as requested. There's also potential to add a test here, of the general functionality of setting generalized costs through modifications, which will indirectly test that the data structure is instantiated when it's missing. I'll re-request review when a test is added.

@trevorgerhardt trevorgerhardt enabled auto-merge (squash) December 14, 2022 09:31
Goal was to create a test would have failed before this PR. It tests general effect of applying a modification with a walk cost factor by checking if there were decreased time to reach routed nodes vs a baseline network.
Copy link
Member Author

@abyrd abyrd left a comment

Choose a reason for hiding this comment

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

Thanks for adding the test @trevorgerhardt. This is the kind of thing I had in mind - check the effect of the modification itself on routing, which indirectly checks that the data tables are created. I think there are a couple of changes needed (see comments). I'll push a commit with some suggested fixes for your review.

var scenario = new Scenario();
scenario.modifications = Lists.newArrayList(ms);

var reachedVertexes = getReachedVertexes(network, new Scenario());
Copy link
Member Author

Choose a reason for hiding this comment

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

Throughout the R5 codebase we have used "vertices" as the plural of "vertex".

totalGreater++;
}
}
Assertions.assertEquals(454, totalGreater);
Copy link
Member Author

Choose a reason for hiding this comment

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

Checking against an exact number probably makes the test more brittle with respect to changes in routing. Ideally we'd do more checks that will verify expected outcomes even after unforeseen changes to routing details - like your check that baselineTime >= modTime. The first checks that come to my mind are: when increasing the travel speed, the post-modification results should always reach many more vertices, all reached vertices in pre-modification results should be present in post-modification results, and travel times should all be higher. Given the high speedup factor, all post-modification travel times should be 1/10 of the pre-modification travel times (could relax to "at most 1/3" to make it more flexible).

Copy link
Member

Choose a reason for hiding this comment

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

I agree, I wasn't totally sure how to structure this test. Thanks for updating that.


int totalGreater = 0;
for (int i = 0; i < reachedVertexes.size(); i++) {
int baselineTime = reachedVertexes.get(i);
Copy link
Member Author

Choose a reason for hiding this comment

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

reachedVertexes is a map from vertex ID to the value of the optimization variable at that vertex. This iteration is using positional indexes in as if they are vertex IDs. Most of the get() calls are probably returning -1 which will be equal in both sets of output.

Copy link
Member

Choose a reason for hiding this comment

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

My mistake! Thanks for correcting that.

use inequalities for softer assertions
style changes, use Arrays.asList()
@trevorgerhardt trevorgerhardt merged commit 76642ab into dev Dec 20, 2022
@trevorgerhardt trevorgerhardt deleted the add-edge-factors-in-modification branch December 20, 2022 18:28
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.

None yet

2 participants