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

Start impact prediction mode #83

Merged
merged 7 commits into from
Jan 17, 2025
Merged

Start impact prediction mode #83

merged 7 commits into from
Jan 17, 2025

Conversation

dabreegster
Copy link
Collaborator

A start to #36. After some changes to one neighbourhood in Edinburgh:
image
The impact is mostly localized around there:
image
Justifying why Queen Street got busier with examples:
image


use crate::{IntersectionID, MapModel, RoadID};

// TODO Rename?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's a few too many things called "impact" in the frontend. Maybe "area-wide traffic prediction"?

}
}

/// Deterministically produce a bunch of OD pairs, just as a fallback when there's no real data
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure yet what approach to take to have real OD data only for some places, will tackle next.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like a reasonable approach for expediency.

It might avoid trouble to regenerate trips where i1 == i2, but maybe it won't be an issue in practice.

@@ -294,6 +295,31 @@ impl LTN {
.map_err(err_to_js)?)
}

/// Returns GJ with a LineString per road, with before/after counts
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

General reflection: It's unsatisfying to have 3 layers where types are expressed either as code or docs:

  1. In the wasm.ts layer as TS
  2. Here in the Rust WASM "interface" layer, which usually just returns JSON strings
  3. In a more internal method on MapModel, where usually we could have more info on types. (If we used https://docs.rs/geojson/latest/geojson/ser/index.html structs, maybe)

It works fine so far, just one of the costs of a cross-language layer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I can see the frustration.

While writing rust, my instinct is that json (including geojson), should only be used as a serialization format to talk to wasm - in other words avoid ever doing application logic with geojson, and do everything with fully expressive rust types.

By that rubrik, this kind of thing is "bad": https://github.com/a-b-street/ltn/pull/83/files#diff-f71d03fb4aef16f1e3c21b14e2c5e077e0d40c8c1af7f897bba4a71f337040d6R87

In typescript I'm less sure - in theory we should be able to write typescript signatures compatible with geojson that also encapsulate our more specific needs (e.g. a LineString Feature that has a "foo: Number" foreign member). (Update: looks like you're already doing this part, great! Feature<LineString, { kind: "before" }>)

Given that I'm brand new to this code base, maybe such a principled stance isn't worth it in practice, so I wouldn't make any radical changes based just on my instincts yet, but consider it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

avoid ever doing application logic with geojson, and do everything with fully expressive rust types.

There's a bunch of other places in the code currently violating this, creating features and doing lots of set_property. The WASM layer in lib.rs is at least the one place to stringify JSON or parse inputs. It would indeed be nice to write structs using geojson::ser for most app-level code to output, especially if we could wire up something like https://docs.rs/ts-rs/latest/ts_rs/ to have more rigor about keeping wasm.ts in sync.

For the moment, I think the codebase is small/simple enough to keep playing it kind of loose, so I won't prioritize it yet.

pub fn predict_impact(&mut self) -> Result<String, JsValue> {
self.map.rebuild_router(1.0);
let mut impact = self.map.impact.take().unwrap();
let out = impact.recalculate(&self.map);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

impact is mutable, but also needs to access other parts of the map model, hence the Option and this "take, then put back" approach. Could be restructured to lift impact out of MapModel, but not too important.

@@ -33,11 +34,11 @@ pub struct MapModel {

// TODO Wasteful, can share some
// This is guaranteed to exist, only Option during MapModel::new internals
pub router_original: Option<Router>,
pub router_before: Option<Router>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed for consistency. "before" means before any user edits are applied. "after" means the current state of user edits.

@@ -14,6 +14,11 @@ pub struct Router {
pub main_road_penalty: f64,
}

pub struct Route {
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 did some detangling here to separately express a route as a sequence of steps and then render as a linestring. I keep reinventing variations of this code for the "OSM-based map model" layer in many projects, and in many others, I've recently consolidated on https://github.com/a-b-street/15m/tree/main/graph/src. It could be later worth trying to migrate this codebase to use that too (and take advantage of all its nice routing and route-rendering bits), but there are complications with doing so (how modal filters are modelled, partly) and it's not really a priority. The general tension is between duplicate code between projects and coming up with something general enough to handle a bunch of projects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is an example how to wire up a new UI mode

Pick a different road
</Link>

<div style="display: flex; justify-content: space-between;">
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Some of this and above could be refactored with ViewShortcuts into some kind of list-navigation, maybe

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 not really happy with any of the styling choices here yet, but it's a start and roughly recreates what's in v1. There's two things we probably want to communicate:

  1. Did a particular road get more or less traffic than before edits? What roads changed the most? A diverging red/green color is used for this.
  2. What's the level of traffic after, in absolute terms?

A road going from 10 trips to 50 trips seems like a dramatic change by color, but 50 trips total is nothing, so it should be thin.

})
.as_object()
.unwrap()
.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

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

minor nit: is this clone necessary - since it's the last thing in the method, I'd expect a move should be sufficient.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sadly yes, as_object returns a ref, and there's no into_object helper

Copy link
Contributor

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

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

Embarrassing question, but how do I get to this view?

@@ -294,6 +295,31 @@ impl LTN {
.map_err(err_to_js)?)
}

/// Returns GJ with a LineString per road, with before/after counts
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I can see the frustration.

While writing rust, my instinct is that json (including geojson), should only be used as a serialization format to talk to wasm - in other words avoid ever doing application logic with geojson, and do everything with fully expressive rust types.

By that rubrik, this kind of thing is "bad": https://github.com/a-b-street/ltn/pull/83/files#diff-f71d03fb4aef16f1e3c21b14e2c5e077e0d40c8c1af7f897bba4a71f337040d6R87

In typescript I'm less sure - in theory we should be able to write typescript signatures compatible with geojson that also encapsulate our more specific needs (e.g. a LineString Feature that has a "foo: Number" foreign member). (Update: looks like you're already doing this part, great! Feature<LineString, { kind: "before" }>)

Given that I'm brand new to this code base, maybe such a principled stance isn't worth it in practice, so I wouldn't make any radical changes based just on my instincts yet, but consider it.

LineString::new(pts)
}

pub fn crosses_road(&self, road: RoadID) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

To make sure I'm understanding our terminology: a RoadID corresponds to a segment of the road network which has no intersections in its interior. Is that right?

So there's no way to "cross" a road without traveling along it.

In common language, traveling "through" an intersection would mean traveling from one RoadID and then switching to another RoadID at the point of the intersection (even though in common language we'd call it the same "road" all along).

The above is all totally fine - just check if I'm misunderstanding.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All correct. It will get a bit more complicated for #30, because the transition from one Road to another Road within an Intersection will have to be modeled explicitly. (Called "turn" or "movement" in abstreet.) Any ideas for more clear terms, happy to use

@dabreegster
Copy link
Collaborator Author

Embarrassing question, but how do I get to this view?

image
From the pick neighbourhood page. I would add it to edit mode too, but the "other related actions" nav is already overflowing:
image

I think the small breadcrumb nav is useful in the top bar, but I'm not really sure where to put the "other actions". The left sidebars get busy too. Other people have had feedback that it's hard to see those actions on the top right.

And to be fair, the old version wasn't really that much better; the top was meant to be mutually exclusive modes, all of which can be jumped to whenever. And the bottom had a few specific to one neighbourhood:
image

I'll leave this alone for now, but ideas welcome

Copy link
Contributor

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

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

Embarrassing question, but how do I get to this view?

Thanks for the instructions! For transparency, I did look in the breadcrumbs, but I was drilled down into the neighborhood screen, not up a level at the "study area" screen.

I think it makes sense where it is, since we measure impact spanning all the neighborhoods.

I think mostly this speaks to me not having a good intuition yet for the data hierarchy.

Let me see if I have it right:

A "project" vs. "study area" are synonymous (or one-to-one).

vs. a neighborhood, which is a subset of a study area. A study area can have many neighborhoods.

When you are placing filters, you are drilled down to "editing a neighborhood", not editing the entire study area.

But because edits in one neighborhood might affect traffic in another, impact measurements must be done at the scale of the entire project/study-area.

Is all that correct?

Anyway, the changes look good, and seem like a good basis for this view. Thanks for walking me through it.

Pick neighbourhood
</Link>
</li>
<li>Predict impact mode</li>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<li>Predict impact mode</li>
<li>Predict impact</li>

"mode" feels redundant in the UI

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

@dabreegster
Copy link
Collaborator Author

Data hierarchy understanding is almost right! It is useful to see the confusing points here, because they're definitely confusing parts of the UX.

A "project" vs. "study area" are synonymous (or one-to-one).

A study area is a place like Edinburgh or a section of Seattle. A project is meant to be like a file somebody makes (and later shares) to use the tool. The project is tied to one study area, but a study area can have multiple files.

Right now, the project has just one set of edits (changes to one-ways, modal filters, turn restrictions, speeds and a definition of neighbourhood boundaries). But in the old tool, there were multiple "proposals" per file/project, and a way to switch quickly between them. I'm not sure we need all that complexity here yet. Something closer to what I'm picturing is https://nptscot.github.io/npw/ (only a few places here have working files online -- Edinburgh or Glasgow, if you're going to try this). First the user restores a project file they were working on previously, or makes a new one by first picking the study area.

When you are placing filters, you are drilled down to "editing a neighborhood", not editing the entire study area.

Correct, but this is probably confusing: #77

But because edits in one neighborhood might affect traffic in another, impact measurements must be done at the scale of the entire project/study-area.

Exactly. The flow a user might take:

  1. Start with a neighbourhood boundary they have in mind already, or use the future neighbourhood prioritization module to pick one.
  2. Within the edit neighbourhood view, edit stuff to achieve low shortcuts and good cell splits. There's pretty instant feedback for this. The problem being solved is traffic cutting through this one area.
  3. Afterwards, someone needs to understand if there could be unexpected spillover effects to nearby neighbourhoods. Plug the leak in one area, but just push traffic to a residential street in the adjacent neighbourhood? No good, need to think through that too. Or push traffic onto the main road (the expected short-term consequence, before "traffic dissipation" can occur) -- maybe there are bus routes there or other issues, and actually a filter on a main perimeter road could be worth thinking about.

--

Thanks for the review! I'll wait for feedback from upstream about "predict impact" vs "predict areawide traffic impacts" or something else before renaming.

@dabreegster dabreegster merged commit 502d6f3 into main Jan 17, 2025
@dabreegster dabreegster deleted the predict_impact branch January 17, 2025 22: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.

2 participants