- Add Cost field to Edge and RouteLeg structs - Propagate Cost in FindRoute itinerary creation - Write TestRouteParetoRanking test verifying non-dominated route selection
165 lines
5.0 KiB
Go
165 lines
5.0 KiB
Go
package routing
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestRouteParetoRanking tests that FindRoutesPareto correctly returns
|
|
// Pareto-optimal routes (non-dominated) based on time, transfers, and cost.
|
|
// A route is dominated if another route is better or equal in all metrics.
|
|
func TestRouteParetoRanking(t *testing.T) {
|
|
graph := NewGraph()
|
|
|
|
// Add stations along a route
|
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
|
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
|
|
|
|
// Direct route: Moscow → Kursk (0 transfers, 3600s, cost 0)
|
|
graph.AddEdge(&Edge{
|
|
From: graph.Nodes()[0], // s1 Moscow
|
|
To: graph.Nodes()[3], // s4 Kursk
|
|
Kind: EdgeKindReal,
|
|
Duration: 3600,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 0,
|
|
})
|
|
|
|
// Indirect route: Moscow → Tula → Vladimir → Kursk (3 transfers, 3*3600=10800s, cost 0)
|
|
graph.AddEdge(&Edge{
|
|
From: graph.Nodes()[0], // s1 Moscow
|
|
To: graph.Nodes()[1], // s2 Tula
|
|
Kind: EdgeKindReal,
|
|
Duration: 3600,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 0,
|
|
})
|
|
graph.AddEdge(&Edge{
|
|
From: graph.Nodes()[1], // s2 Tula
|
|
To: graph.Nodes()[2], // s3 Vladimir
|
|
Kind: EdgeKindReal,
|
|
Duration: 3600,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 0,
|
|
})
|
|
graph.AddEdge(&Edge{
|
|
From: graph.Nodes()[2], // s3 Vladimir
|
|
To: graph.Nodes()[3], // s4 Kursk
|
|
Kind: EdgeKindReal,
|
|
Duration: 3600,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 0,
|
|
})
|
|
|
|
// Fast but expensive route: Moscow → Tula (1 leg, 1800s, cost 5000)
|
|
// This would be an alternative direct route with higher cost but lower duration
|
|
// Add a second direct edge with different characteristics if needed
|
|
|
|
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
|
results := graph.FindRoutesPareto("s1", "s4", opts)
|
|
|
|
// Should find at least the direct route (0 transfers, 3600s)
|
|
if len(results) == 0 {
|
|
t.Error("expected at least 1 Pareto-optimal route")
|
|
}
|
|
|
|
// The direct route (0 transfers, 3600s) should be Pareto-optimal
|
|
// since no other route has both fewer transfers and less duration
|
|
directFound := false
|
|
for _, r := range results {
|
|
if r.TotalDuration == 3600 && r.TotalTransfers == 0 {
|
|
directFound = true
|
|
break
|
|
}
|
|
}
|
|
if !directFound {
|
|
t.Error("expected direct route (0 transfers, 3600s) in Pareto results")
|
|
}
|
|
|
|
// Test with routes that have different cost values
|
|
graph2 := NewGraph()
|
|
graph2.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
|
graph2.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
|
graph2.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
|
graph2.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
|
|
|
|
// Route A: 0 transfers, 3600s, cost 1000
|
|
graph2.AddEdge(&Edge{
|
|
From: graph2.Nodes()[0],
|
|
To: graph2.Nodes()[3],
|
|
Kind: EdgeKindReal,
|
|
Duration: 3600,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 1000,
|
|
})
|
|
|
|
// Route B: 0 transfers, 4000s, cost 0 (cheaper but slower)
|
|
// This route should NOT dominate Route A (different cost), and Route A
|
|
// should NOT dominate Route B (Route A is faster but more expensive)
|
|
graph2.AddEdge(&Edge{
|
|
From: graph2.Nodes()[0],
|
|
To: graph2.Nodes()[3],
|
|
Kind: EdgeKindReal,
|
|
Duration: 4000,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 0,
|
|
})
|
|
|
|
// Route C: 1 transfer, 3000s, cost 0 (middle ground)
|
|
graph2.AddEdge(&Edge{
|
|
From: graph2.Nodes()[0],
|
|
To: graph2.Nodes()[1],
|
|
Kind: EdgeKindReal,
|
|
Duration: 2000,
|
|
Transport: "train",
|
|
IsTransfer: false,
|
|
Cost: 0,
|
|
})
|
|
graph2.AddEdge(&Edge{
|
|
From: graph2.Nodes()[1],
|
|
To: graph2.Nodes()[3],
|
|
Kind: EdgeKindReal,
|
|
Duration: 1000,
|
|
Transport: "train",
|
|
IsTransfer: true,
|
|
Cost: 0,
|
|
})
|
|
|
|
opts2 := SearchOptions{MaxTransfers: 3, MCT: 300}
|
|
results2 := graph2.FindRoutesPareto("s1", "s4", opts2)
|
|
|
|
// Should find at least some routes
|
|
if len(results2) == 0 {
|
|
t.Error("expected at least 1 Pareto-optimal route with cost variation")
|
|
}
|
|
|
|
// Verify no route is dominated by another in all metrics
|
|
for i, r1 := range results2 {
|
|
for j, r2 := range results2 {
|
|
if i == j {
|
|
continue
|
|
}
|
|
// Check if r2 dominates r1
|
|
r2DominatesR1 := r2.TotalDuration <= r1.TotalDuration &&
|
|
r2.TotalTransfers <= r1.TotalTransfers &&
|
|
r2.Cost <= r1.Cost &&
|
|
(r2.TotalDuration < r1.TotalDuration ||
|
|
r2.TotalTransfers < r1.TotalTransfers ||
|
|
r2.Cost < r1.Cost)
|
|
if r2DominatesR1 {
|
|
t.Errorf("route %d should not be dominated by route %d: r2 dominates r1 "+
|
|
"(dur:%d vs %d, transf:%d vs %d, cost:%d vs %d)",
|
|
i, j, r1.TotalDuration, r2.TotalDuration,
|
|
r1.TotalTransfers, r2.TotalTransfers,
|
|
r1.Cost, r2.Cost)
|
|
}
|
|
}
|
|
}
|
|
} |