Skip to content

Conversation

@miklcct
Copy link
Contributor

@miklcct miklcct commented Aug 6, 2025

Summary

This removes the safety normalizer. The normalizer class still exists but it now just collects the minimum safety values instead of changing the edge.

The heuristic function is also updated to take the reluctance and minimum safety into account, so it will still be admissible even with a low reluctance (e.g. if the user sets walk reluctance to 0.6).

As mentioned in #4442,

  • We might need to update all the safety values in way property set sources because the values would now be so different without the normalization
  • At least cycling reluctance values used by client might have to be changed to have the same reluctance in practice

Our client app has the ability to submit custom reluctance values, and explained to the users what it is supposed to do. For example, our app has the following explanation:

image

The safety normalizer has resulted in cyclists complaining because it shows the cycling journeys much more stressful then they thought, even if it is on a safe route.

Issue

Fixes #6775

Unit tests

Added extensively

Documentation

Changelog

Bumping the serialization version id

Needed. The minimum safety values for walk and cycle are now stored in the graph.

@codecov
Copy link

codecov bot commented Aug 6, 2025

Codecov Report

❌ Patch coverage is 79.72973% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.18%. Comparing base (7d31996) to head (9b738fd).
⚠️ Report is 35 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...r/graph_builder/module/osm/SafetyValueApplier.java 72.72% 9 Missing and 6 partials ⚠️
...ch/strategy/EuclideanRemainingWeightHeuristic.java 78.00% 9 Missing and 2 partials ⚠️
...anner/street/model/StreetLimitationParameters.java 66.66% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #6782      +/-   ##
=============================================
+ Coverage      72.15%   72.18%   +0.02%     
- Complexity     19782    19848      +66     
=============================================
  Files           2151     2156       +5     
  Lines          79981    80097     +116     
  Branches        8061     8075      +14     
=============================================
+ Hits           57713    57816     +103     
- Misses         19422    19432      +10     
- Partials        2846     2849       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@t2gran t2gran added this to the 2.8 (next release) milestone Aug 11, 2025
@miklcct miklcct marked this pull request as ready for review August 22, 2025 14:35
@miklcct miklcct requested a review from a team as a code owner August 22, 2025 14:35
@miklcct miklcct marked this pull request as draft August 22, 2025 14:39
# Conflicts:
#	application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java
#	application/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap
@miklcct miklcct marked this pull request as ready for review August 22, 2025 14:57
@optionsome optionsome self-requested a review August 26, 2025 09:20
Copy link
Member

@optionsome optionsome left a comment

Choose a reason for hiding this comment

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

Removing the safety normalizer in practice reduces the cost of cycling a bit. This effect could obviously be balanced by manually updating the bicycle safety factor values to be slightly higher, but I'm not sure if we want to do that. We could also adjust the default cycling reluctance to be higher but that would only affect those who use the default.

* Further reading: <a href="https://github.com/opentripplanner/OpenTripPlanner/issues/4442">Issue 4442</a>
* <a href="https://github.com/opentripplanner/OpenTripPlanner/issues/6775">Issue 6775</a>
*/
class SafetyValueNormalizer {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should rename this as this has now been repurposed. I tried to about a name for this but couldn't come up with one quickly. You can try to come up with a new name or we can decide in a dev meeting.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should call this SafetyValueApplier?

public EuclideanRemainingWeightHeuristic(Float maxCarSpeed) {
this.maxCarSpeed = maxCarSpeed != null ? maxCarSpeed : DEFAULT_MAX_CAR_SPEED;
public EuclideanRemainingWeightHeuristic(
StreetLimitationParametersService streetLimitationParametersService
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if we should pass in this service or just the three parameters that are relevant here.

Comment on lines +9 to +24
StreetLimitationParametersService DEFAULT = new StreetLimitationParametersService() {
@Override
public float getMaxCarSpeed() {
return DEFAULT_MAX_CAR_SPEED;
}

@Override
public float getBestWalkSafety() {
return 1.0f;
}

@Override
public float getBestBikeSafety() {
return 1.0f;
}
};
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit of an unusual approach for constructing defaults. I'm not sure if we are ok with this, we can discuss it in a dev meeting.

# Conflicts:
#	application/src/main/java/org/opentripplanner/graph_builder/module/osm/SafetyValueNormalizer.java
#	application/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap
@optionsome optionsome requested a review from abyrd September 2, 2025 09:36
@abyrd
Copy link
Member

abyrd commented Sep 4, 2025

Commenting after a little discussion in last Tuesday's developer meeting. This PR and the referenced issue #4442 show that normalization could be removed, and how it could be removed while keeping heuristics admissible. But the question arises of why we would want to remove it, and whether the benefits are significant enough to motivate the surrounding changes. Are the reasons still the same as stated in #4442? Namely:

  • Difficult to know what the safety values will be, as they look different than those in the property set
  • Safety values always act as reluctance factor, never as a preference

In that ticket, these are described as a "minor inconvenience" and a decision to not change the normalization or heuristics was documented there. In addition, @t2gran commented that he did not want to remove the normalization and found it simpler to reason about.

It seems that a decision to make these changes would be dependent on overriding that past decision. Is there a reason removing normalization has become more pressing now? I see that there is some issue communicating with end users about the meaning, but might that be resolved by rewording the explanations? I also don't think these reluctance parameters were ever meant to be exposed directly to end users - we should make sure this is an intentional decision.

@miklcct
Copy link
Contributor Author

miklcct commented Sep 4, 2025

The reasons stated in #4442 have become a serious problem for us when we are trying to introduce the safety feature to our users. All the bicycle journeys got inflated generalized cost resulting in the backend suggesting taking transport even when the user wants to cycle more, because there are a few ways with exceptionally low safety values such as 0.25 due to some combination of tags not foreseen when the mapper was written, resulting in normal roads forming the majority of the map inflated from 1 to 4.

This normalizing has complicated the matter for both the people making the mapper, and the user needing to set an appropriate value, so it should be removed.

The ability to set a custom reluctance factor on a sliding scale is the whole reason why Aubin exists nowadays.

@abyrd
Copy link
Member

abyrd commented Sep 4, 2025

I see that there is some issue communicating with end users about the meaning, but might that be resolved by rewording the explanations?

To clarify what I mean here:
Your screenshot shows the text "1.8x reluctant means that you prefer 18 minutes travelling on a public transport vehicle to 10 minutes of walking." This phrasing may be hard for end users to interpret. The base units for our generalized cost are seconds of seated in-vehicle transit travel time. So I have always explained a reluctance factor of 1.8 as "you consider walking for ten minutes to be as bad as riding for 18 minutes in a bus".

@miklcct
Copy link
Contributor Author

miklcct commented Sep 4, 2025

Yes. I may need to adjust the wording however the intention is the same.

The problem with the normalizer here is that, if there exists even one single road in the map with safety value 0.25, all roads with default safety (i.e. 1.0) suddenly becomes 4.0 in practice, so the system thinks that all roads are dangerous and tells the user to take public transport instead, if the user wants to use a safe route.

@abyrd
Copy link
Member

abyrd commented Sep 4, 2025

All the bicycle journeys got inflated generalized cost resulting in the backend suggesting taking transport even when the user wants to cycle more, because there are a few ways with exceptionally low safety values such as 0.25 due to some combination of tags not foreseen when the mapper was written, resulting in normal roads forming the majority of the map inflated from 1 to 4.

It's been a while since I used any safety factors so I wasn't understanding the problem at first. I now see the problem. Although the reluctance factors are multiplicative factors, so have the same relative effect on street routing independent of the cost of a "normal" road", those renormalized bike safety costs are being directly compared against (and combined with) transit costs that are in uninflated seconds of in-vehicle time.

On some level it seems reasonably correct though. Bicycling on the best, safest bicycle path around is the reference point for a good safe bicycle ride, on par with riding inside a transit vehicle, and in a context where such paths exist, bicycling on normal roads with car traffic really is 4 times worse (in reality possibly much worse than that, a quick look at the literature makes me think a factor of 25 is reasonable). I am a little hesitant about the meaning of sub-1.0 factors for safe bike paths, with 1.0 factors for "normal" roads. Doesn't this imply that riding a bicycle on a car road is just as safe as riding inside a transit vehicle?

Again, it's been a long time since I used these factors so I'm looking into it now to refresh my understanding in the interest of reviewing this PR.

@abyrd
Copy link
Member

abyrd commented Sep 4, 2025

the system thinks that all roads are dangerous and tells the user to take public transport instead, if the user wants to use a safe route.

Well, all roads that are not bicycle paths completely separated from traffic are dangerous to bicyclists. Generally vastly more dangerous than being inside a metal transit vehicle. Do we want the safety model to align with people's (often incorrect) intuitive perception of safety, rather than measurable reality? People underestimate how extremely dangerous being in the same space with moving cars is. In the same way people are afraid of flying but vastly more likely to be killed crossing the street on the way to the airport.

@miklcct
Copy link
Contributor Author

miklcct commented Sep 4, 2025

We can further discuss this later - when safety factor is 1.0 and bike reluctance is 1.0, shall we treat cycling on normal roads the same as taking transit (making cycling on safe streets preferable to transit), or cycling on safest streets the same as taking transit (making cycling on normal roads worse than taking transit). I can accept both options and would like the community to decide, as I can easily explain to the user.

There is also a problem with the "safest streets" option which is supposed to exaggerate the safety even more than normal - it never works with the normalizer so in effect it is the same as "safe streets", and I have a follow up PR to fix this problem.

@miklcct
Copy link
Contributor Author

miklcct commented Sep 6, 2025

An objective measure of safety factors is the death statistics. For example, if a death (someone getting killed) is 5x more likely on a certain class of road compared to riding transit for the same duration, the safety factor of that class of the road would be 5.

The effect is that, if we choose a safe route, we are minimalising the likelihood of losing lives.

If we want to adopt this, not only we still have to remove the normalizer, but we also need to introduce safety factors for car driving as well (because driving a car is also more deadly than taking transit).

@t2gran t2gran modified the milestones: 2.8, 2.9 (next release) Sep 10, 2025
@optionsome
Copy link
Member

Summary of the discussion in the dev meeting @t2gran:

  • This pr will lower the safety factors that are in use since they are no longer normalized
    • This means that the "total" bicycle reluctance will be lower in practice if we don't do anything
  • There will be a follow up pr that will attempt to adjust the request bicycle reluctancies, but it is not sure yet if that approach will work (safety value is not used in all bicycle routing requests while reluctance can be)
    • Other alternative is to adjust to safety factors in the tag mappers

@optionsome optionsome self-requested a review September 23, 2025 10:00
* Further reading: <a href="https://github.com/opentripplanner/OpenTripPlanner/issues/4442">Issue 4442</a>
* <a href="https://github.com/opentripplanner/OpenTripPlanner/issues/6775">Issue 6775</a>
*/
class SafetyValueNormalizer {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should call this SafetyValueApplier?


private double getCyclingCostPerDistance(BikePreferences preferences) {
return getCostPerDistance(
preferences.speed(),
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we also scale the speed based on the triangle preferences? It's a bit complex problem what we should and shouldn't take into account here since there is also the slope factor, and we don't really want to overestimate the minimum cost here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be too complicated to take elevation into the account when calculating the heuristic. It may pose a problem when the journey is totally downhill.

If anyone knows how I can guess the minimum for a downhill value I am happy to implement it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The speed always affects the cost, it is not relevant to the triangle preference.

Copy link
Member

Choose a reason for hiding this comment

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

Again the original concept was: all configurable cost factors should only ever increase cost. So specifically: adding elevation data should only ever increase cost above the baseline, not decrease it. If you are applying cost-transforms that can decrease cost (yield fractional multiplers in [0, 1)), the minimum factor would need to be found and accounted for in the heuristic. Otherwise the heuristic is just not "admissible" and the algorithm is going to return suboptimal paths. Strictly speaking coasting downhill is easier than rolling on perfectly flat ground, but is it more important to model this "cost rebate" of coasting downhill, or to keep your heuristic admissible? Priorities will need to be chosen.

Copy link
Member

@t2gran t2gran Oct 13, 2025

Choose a reason for hiding this comment

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

I think our strategy so fare has been to "keep your heuristic admissible" - I do not want to change the strategy, unless there is a very good reason - preferable analyzed and described in a document or issue summary.

# Conflicts:
#	application/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap
@miklcct miklcct requested a review from optionsome October 1, 2025 10:27
# Conflicts:
#	application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java
#	application/src/main/java/org/opentripplanner/routing/impl/GraphPathFinder.java
#	application/src/test/java/org/opentripplanner/astar/AStarTest.java
#	application/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/BikeRentalSnapshotTest.snap
#	application/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap
#	application/src/test/java/org/opentripplanner/street/integration/BikeRentalTest.java
@abyrd
Copy link
Member

abyrd commented Oct 15, 2025

This was discussed at length in the developer meeting on 2025-10-14. Among those with any opinion on the subject, there is support for merging the changes in this PR. But it would need to be combined with additional changes that would adjust any defaults and hard-coded values, making the sequence of commits a pure refactor with no confusing side-effects from the perspective of people deploying OTP. This PR should not be merged into dev until those follow-up changes have also been thought through and approved. So probably two PRs that will be merged at once.

@miklcct
Copy link
Contributor Author

miklcct commented Oct 15, 2025

This was discussed at length in the developer meeting on 2025-10-14. Among those with any opinion on the subject, there is support for merging the changes in this PR. But it would need to be combined with additional changes that would adjust any defaults and hard-coded values, making the sequence of commits a pure refactor with no confusing side-effects from the perspective of people deploying OTP. This PR should not be merged into dev until those follow-up changes have also been thought through and approved. So probably two PRs that will be merged at once.

It is impossible to give an accurate number after fixing this because the effect depends on the OSM used. This is why the normalizer was problematic as loading a smaller or larger OSM can result in the same area behave differently.

Meanwhile I am mentioning #6767 here as it is the PR which changes the hardcoded values.

@t2gran
Copy link
Member

t2gran commented Oct 16, 2025

I will need a migration guide, also it is important that this PR is not blocking us from using dev-2.x for a long time - a few days or a week is ok. But, for us it is important that when this is merged any follow up PRs witch affect the configuratio is also merged relativly soon after. The migration guide must be available before any of these PRs are merged.

@miklcct
Copy link
Contributor Author

miklcct commented Oct 16, 2025

I will need a migration guide, also it is important that this PR is not blocking us from using dev-2.x for a long time - a few days or a week is ok. But, for us it is important that when this is merged any follow up PRs witch affect the configuratio is also merged relativly soon after. The migration guide must be available before any of these PRs are merged.

I have added documentation of how the safety values actually work in practice, which now become clear when the normalization is removed (they are applied as a multiplier on top of the reluctance).

My aim is to change the values of the tag mappers in #6767 so that the configurations won't need to be changed for similar effect. In this case, it will create an impression that cycling on most urban roads (e.g. secondary or tertiary) is actually undesirable, and I believe this is what the community wants.

@miklcct miklcct force-pushed the remove-safety-normalizer branch 2 times, most recently from 7b5cf8b to ad3842b Compare October 16, 2025 12:51
@miklcct miklcct force-pushed the remove-safety-normalizer branch from ad3842b to 9b738fd Compare October 16, 2025 13:58
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.

Safety value normalizer inflates generalizedCost

4 participants