trips/router/internal/plan/plan_test.go
Greg Pomerantz efde2cc71b maps project: design, survey, mock app, and route-aware planning backend
- DESIGN.md: full design incl. driving-trip requirements (R1-R4),
  stays model, focus mode, mobile, provenance rules
- SURVEY.md: open-source landscape
- mock/: interaction mock (Florence itinerary, focus mode, stays,
  region stops, mobile layout)
- router/: Go module (stdlib-only) with Router interface
  (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor,
  routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing),
  integration tests, and setup-osrm.sh for the self-hosted router
- osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
2026-09-06 00:05:17 -04:00

146 lines
4.0 KiB
Go

package plan
import (
"testing"
"maps/router/internal/route"
)
// synthMatrix builds a (n+2)-layout matrix from a full (n+2)² symmetric
// matrix in seconds.
func synthMatrix(vals [][]float64) route.Matrix {
m := make(route.Matrix, len(vals))
for i := range m {
m[i] = append([]float64(nil), vals[i]...)
}
return m
}
// 3 stops, A=3, C=4.
// Travel times (seconds):
//
// A→C direct: 3600 (60 min)
// A→S0 600, S0→C 3600 → detour 600 (10 min)
// A→S1 1200, S1→C 3600 → detour 1200 (20 min)
// A→S2 1800, S2→C 3600 → detour 1800 (30 min)
// S0→S1 600, S1→S2 600, S0→S2 1200
func testMatrix() route.Matrix {
return synthMatrix([][]float64{
{0, 600, 1200, 600, 3600}, // S0
{600, 0, 600, 1200, 3600}, // S1
{1200, 600, 0, 1800, 3600}, // S2
{600, 1200, 1800, 0, 3600}, // A
{3600, 3600, 3600, 3600, 0}, // C
})
}
var testStops = []Stop{
{ID: "s0", Name: "S0", DwellMin: 30},
{ID: "s1", Name: "S1", DwellMin: 30},
{ID: "s2", Name: "S2", DwellMin: 30},
}
func TestStopCost(t *testing.T) {
m := testMatrix()
c := StopCost(m, 0, testStops[0])
if c.DetourMin != 10 {
t.Errorf("S0 detour = %d, want 10", c.DetourMin)
}
if c.DirectMin != 60 {
t.Errorf("direct = %d, want 60", c.DirectMin)
}
if c.TotalMin != 10+30+10 {
t.Errorf("total = %d, want 50", c.TotalMin)
}
c2 := StopCost(m, 2, testStops[2])
if c2.DetourMin != 30 {
t.Errorf("S2 detour = %d, want 30", c2.DetourMin)
}
}
func TestSeqCostOrderMatters(t *testing.T) {
m := testMatrix()
// A→S0→S1→C: 600+600+3600 = 4800 vs 3600 → detour 1200 s = 20 min
d, total := SeqCost(m, []int{0, 1}, testStops)
if d != 20 {
t.Errorf("detour = %d, want 20", d)
}
if total != 20+30+10+30+10 {
t.Errorf("total = %d, want 100", total)
}
}
func TestOptimizeExhaustivePicksmm(t *testing.T) {
m := testMatrix()
// k=1: best single stop is S0 (detour 10 + dwell 30 + ovh 10 = 50)
r := OptimizeStops(m, testStops, 1, 0)
if len(r.Order) != 1 || r.Order[0] != 0 {
t.Fatalf("k=1 order = %v, want [0]", r.Order)
}
if r.TotalMin != 50 {
t.Errorf("k=1 total = %d, want 50", r.TotalMin)
}
// k=2: best pair: [0,1]: detour 20, total 20+80 = 100
// [0,2]: A→S0(600)+S0→S2(1200)+S2→C(3600)=5400 → detour 30 min, total 30+80=110
// [1,2]: 1200+600+3600=5400 → detour 30, total 110
r2 := OptimizeStops(m, testStops, 2, 0)
if len(r2.Order) != 2 || r2.Order[0] != 0 || r2.Order[1] != 1 {
t.Fatalf("k=2 order = %v, want [0 1]", r2.Order)
}
if r2.TotalMin != 100 {
t.Errorf("k=2 total = %d, want 100", r2.TotalMin)
}
}
func TestOptimizeBudgetFeasibilityFirst(t *testing.T) {
m := testMatrix()
// budget 55 min: k=1 → S0 (50) feasible; k=2 → [0,1] (100) infeasible,
// so best feasible is k=1 S0 (50).
r := OptimizeStops(m, testStops, 2, 55)
if len(r.Order) != 1 || r.Order[0] != 0 {
t.Fatalf("budget 55: order = %v, want [0]", r.Order)
}
if r.TotalMin != 50 {
t.Errorf("total = %d, want 50", r.TotalMin)
}
}
// TestOptimizeGreedyMatchesExhaustive checks the greedy fallback
// (n > 10) against the exhaustive optimum on a small-ish case forced
// through both code paths.
func TestOptimizeGreedyMatchesExhaustive(t *testing.T) {
// 12 stops on a line: A→C 60 min, stops evenly placed, dwell 20 each.
n := 12
vals := make([][]float64, n+2)
// place on a line: position 0..13; A at 0, C at 13, stops at 1..12.
pos := make([]float64, n+2)
for i := 0; i < n; i++ {
pos[i] = float64(i + 1)
}
pos[n] = 0
pos[n+1] = 13
for i := 0; i < n+2; i++ {
vals[i] = make([]float64, n+2)
for j := 0; j < n+2; j++ {
d := pos[i] - pos[j]
if d < 0 {
d = -d
}
vals[i][j] = d * 600 // 10 min per unit
}
}
m := synthMatrix(vals)
stops := make([]Stop, n)
for i := range stops {
stops[i] = Stop{ID: string(rune('a' + i)), DwellMin: 20}
}
k := 3
exp := optimizeExhaustive(m, stops, k, 0)
// force greedy by calling with n > exhaustiveMax
got := optimizeGreedy(m, stops, k, 0)
if exp.TotalMin != got.TotalMin {
t.Errorf("greedy total %d != exhaustive total %d (order %v vs %v)",
got.TotalMin, exp.TotalMin, got.Order, exp.Order)
}
}