Try a brute-force search for the best single filter placement. Also not

great.
This commit is contained in:
Dustin Carlino 2022-01-11 09:40:54 +00:00
parent 65899f3a79
commit afc9846c1c
2 changed files with 137 additions and 50 deletions

View File

@ -1,32 +1,64 @@
//! Experiments to make a neighborhood be low-traffic by automatically placing filters to prevent all rat runs.
use abstutil::Timer;
use widgetry::EventCtx;
use map_model::RoadID;
use widgetry::{Choice, EventCtx};
use super::rat_runs::find_rat_runs;
use super::Neighborhood;
use crate::app::App;
/// Find the road with the most rat-runs that can be closed without creating a disconnected cell,
/// and filter it. There's a vague intuition or observation that the "bottleneck" will have the
/// most rat-runs going through it, so tackle the worst problem first.
pub fn greedy_heuristic(
ctx: &EventCtx,
app: &mut App,
neighborhood: &Neighborhood,
timer: &mut Timer,
) {
if neighborhood
.cells
.iter()
.filter(|c| c.is_disconnected())
.count()
!= 0
{
warn!("Not applying the greedy heuristic to a neighborhood; it already has a disconnected cell");
return;
#[derive(Clone, Debug, PartialEq)]
pub enum Heuristic {
/// Find the road with the most rat-runs that can be closed without creating a disconnected
/// cell, and filter it.
///
/// There's a vague intuition or observation that the "bottleneck" will have the most rat-runs
/// going through it, so tackle the worst problem first.
Greedy,
/// Try adding one filter to every possible road, counting the rat-runs after. Choose the next
/// step by the least resulting rat runs.
BruteForce,
}
impl Heuristic {
pub fn choices() -> Vec<Choice<Heuristic>> {
vec![
Choice::new("greedy", Heuristic::Greedy),
Choice::new("brute-force", Heuristic::BruteForce),
]
}
pub fn apply(
self,
ctx: &EventCtx,
app: &mut App,
neighborhood: &Neighborhood,
timer: &mut Timer,
) {
if neighborhood
.cells
.iter()
.filter(|c| c.is_disconnected())
.count()
!= 0
{
warn!(
"Not automatically changing this neighborhood; it already has a disconnected cell"
);
return;
}
// TODO If we already have no rat-runs, stop
match self {
Heuristic::Greedy => greedy(ctx, app, neighborhood, timer),
Heuristic::BruteForce => brute_force(ctx, app, neighborhood, timer),
}
}
}
fn greedy(ctx: &EventCtx, app: &mut App, neighborhood: &Neighborhood, timer: &mut Timer) {
let rat_runs = find_rat_runs(
&app.primary.map,
&neighborhood,
@ -42,25 +74,68 @@ pub fn greedy_heuristic(
.iter()
.max_by_key(|pair| pair.1)
{
let road = app.primary.map.get_r(*r);
app.session
.modal_filters
.roads
.insert(road.id, road.length() / 2.0);
let new_neighborhood = Neighborhood::new(ctx, app, neighborhood.orig_perimeter.clone());
if new_neighborhood
.cells
.iter()
.filter(|c| c.is_disconnected())
.count()
!= 0
{
warn!("Filtering {} disconnects a cell, never mind", road.id);
app.session.modal_filters.roads.remove(&road.id).unwrap();
if try_to_filter_road(ctx, app, neighborhood, *r).is_none() {
warn!("Filtering {} disconnects a cell, never mind", r);
// TODO Try the next choice
}
}
}
// TODO The brute force approach: try to filter every possible road, find the one with the least
// rat-runs by the end
fn brute_force(ctx: &EventCtx, app: &mut App, neighborhood: &Neighborhood, timer: &mut Timer) {
// Which road leads to the fewest rat-runs?
let mut best: Option<(RoadID, usize)> = None;
let orig_filters = app.session.modal_filters.roads.len();
timer.start_iter(
"evaluate candidate filters",
neighborhood.orig_perimeter.interior.len(),
);
for r in &neighborhood.orig_perimeter.interior {
timer.next();
if app.session.modal_filters.roads.contains_key(r) {
continue;
}
if let Some(new) = try_to_filter_road(ctx, app, neighborhood, *r) {
let num_rat_runs =
// This spams too many logs, and can't be used within a start_iter anyway
find_rat_runs(&app.primary.map, &new, &app.session.modal_filters, &mut Timer::throwaway())
.paths
.len();
// TODO Again, break ties. Just the number of paths is kind of a weak metric.
if best.map(|(_, score)| num_rat_runs < score).unwrap_or(true) {
best = Some((*r, num_rat_runs));
}
// Always undo the new filter between each test
app.session.modal_filters.roads.remove(r).unwrap();
}
assert_eq!(orig_filters, app.session.modal_filters.roads.len());
}
if let Some((r, _)) = best {
try_to_filter_road(ctx, app, neighborhood, r).unwrap();
}
}
// If successful, returns a Neighborhood and leaves the new filter in place. If it disconncts a
// cell, reverts the change and returns None
fn try_to_filter_road(
ctx: &EventCtx,
app: &mut App,
neighborhood: &Neighborhood,
r: RoadID,
) -> Option<Neighborhood> {
let road = app.primary.map.get_r(r);
app.session
.modal_filters
.roads
.insert(r, road.length() / 2.0);
// TODO This is expensive; can we just do the connectivity work and not drawing?
let new_neighborhood = Neighborhood::new(ctx, app, neighborhood.orig_perimeter.clone());
if new_neighborhood.cells.iter().any(|c| c.is_disconnected()) {
app.session.modal_filters.roads.remove(&r).unwrap();
None
} else {
Some(new_neighborhood)
}
}

View File

@ -4,6 +4,7 @@ use widgetry::{
EventCtx, GeomBatch, GfxCtx, Key, Outcome, Panel, State, Text, TextExt, Toggle, Widget,
};
use super::auto::Heuristic;
use super::per_neighborhood::{FilterableObj, Tab, TakeNeighborhood};
use super::Neighborhood;
use crate::app::{App, Transition};
@ -36,12 +37,20 @@ impl Viewer {
Toggle::choice(ctx, "draw cells", "areas", "streets", Key::D, true),
]),
Text::new().into_widget(ctx).named("warnings"),
ctx.style()
.btn_outline
.text("Automatically stop rat-runs")
.hotkey(Key::A)
.tooltip("Warning: uses experimental heuristics to place filters")
.build_def(ctx),
Widget::row(vec![
Widget::dropdown(
ctx,
"heuristic",
// TODO Session state
Heuristic::Greedy,
Heuristic::choices(),
),
ctx.style()
.btn_outline
.text("Automatically stop rat-runs")
.hotkey(Key::A)
.build_def(ctx),
]),
]),
)
.build(ctx);
@ -85,7 +94,8 @@ impl State<App> for Viewer {
Outcome::Clicked(x) => {
if x == "Automatically stop rat-runs" {
ctx.loading_screen("automatically filter a neighborhood", |ctx, timer| {
crate::ltn::auto::greedy_heuristic(ctx, app, &self.neighborhood, timer);
let heuristic: Heuristic = self.panel.dropdown_value("heuristic");
heuristic.apply(ctx, app, &self.neighborhood, timer);
});
self.neighborhood =
Neighborhood::new(ctx, app, self.neighborhood.orig_perimeter.clone());
@ -97,13 +107,15 @@ impl State<App> for Viewer {
.handle_action::<Viewer>(ctx, app, x.as_ref())
.unwrap();
}
Outcome::Changed(_) => {
self.world = make_world(
ctx,
app,
&self.neighborhood,
self.panel.is_checked("draw cells"),
);
Outcome::Changed(x) => {
if x == "draw cells" {
self.world = make_world(
ctx,
app,
&self.neighborhood,
self.panel.is_checked("draw cells"),
);
}
}
_ => {}
}