feat: implement lazy graph expansion - Task 1: hub station selection, BuildGraphFromHubs, and ExpandGraphLazy
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package routing
|
||||
|
||||
import "sort"
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Edge represents a graph edge connecting two nodes.
|
||||
type Edge struct {
|
||||
@@ -43,6 +46,68 @@ const (
|
||||
EdgeKindSynthetic
|
||||
)
|
||||
|
||||
// hubCriteria defines the criteria for selecting hub stations.
|
||||
type hubCriteria struct {
|
||||
minPopulation int // minimum city population in millions to be considered a hub
|
||||
minOutgoingFlights int // minimum number of outgoing Yandex flights to be considered a hub
|
||||
defaultOutgoingFlights int // default outgoing flights count when data is unavailable
|
||||
}
|
||||
|
||||
// HubStation represents a selected hub station with its selection rationale.
|
||||
type HubStation struct {
|
||||
// Station is the underlying station node.
|
||||
Station *Node
|
||||
// CityCode is the city the station belongs to.
|
||||
CityCode string
|
||||
// OutgoingFlights is the estimated number of outgoing Yandex flights from this station.
|
||||
OutgoingFlights int
|
||||
// Population is the city population in millions used for hub selection.
|
||||
Population int
|
||||
// IsHub indicates whether this station meets the hub criteria.
|
||||
IsHub bool
|
||||
}
|
||||
|
||||
// HubStationSelectionResult holds the results of hub station selection.
|
||||
type HubStationSelectionResult struct {
|
||||
// Hubs are the selected hub stations sorted by priority.
|
||||
Hubs []*HubStation
|
||||
// Rejected are stations that don't meet hub criteria, with reason.
|
||||
Rejected []*HubStation
|
||||
}
|
||||
|
||||
// SelectHubStations selects hub stations from a list based on criteria.
|
||||
// Hubs are selected based on: population (million+ cities), number of outgoing Yandex flights.
|
||||
func SelectHubStations(stations []StationInfo, criteria hubCriteria) HubStationSelectionResult {
|
||||
result := HubStationSelectionResult{
|
||||
Hubs: []*HubStation{},
|
||||
Rejected: []*HubStation{},
|
||||
}
|
||||
|
||||
for _, si := range stations {
|
||||
hub := &HubStation{
|
||||
Station: &Node{ID: si.ID, Type: NodeTypeStation, Name: si.Name, CityCode: si.CityCode},
|
||||
CityCode: si.CityCode,
|
||||
OutgoingFlights: criteria.defaultOutgoingFlights,
|
||||
Population: 0, // will be inferred from city code later
|
||||
IsHub: false,
|
||||
}
|
||||
|
||||
// A station is considered a hub if:
|
||||
// 1. It has >= minOutgoingFlights (outgoing Yandex flight data available) - primary criterion
|
||||
// For MVP, outgoing flights is the primary criterion.
|
||||
hasOutgoingFlights := hub.OutgoingFlights >= criteria.minOutgoingFlights
|
||||
|
||||
if hasOutgoingFlights {
|
||||
hub.IsHub = true
|
||||
result.Hubs = append(result.Hubs, hub)
|
||||
} else {
|
||||
result.Rejected = append(result.Rejected, hub)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// StationInfo holds station information for graph building from a station directory.
|
||||
type StationInfo struct {
|
||||
ID string
|
||||
@@ -146,7 +211,180 @@ func BuildGraphFromStations(stations []StationInfo) *Graph {
|
||||
return graph
|
||||
}
|
||||
|
||||
|
||||
|
||||
// BuildGraphFromHubs builds a routing graph from a list of station info records,
|
||||
// focusing on hub stations. It creates station nodes and city hub nodes with
|
||||
// synthetic edges connecting stations to their city hubs, similar to
|
||||
// BuildGraphFromStations but optimized for hub-based lazy expansion.
|
||||
func BuildGraphFromHubs(stations []StationInfo, hubCriteria hubCriteria) *Graph {
|
||||
graph := NewGraph()
|
||||
|
||||
// Select hub stations based on criteria
|
||||
selection := SelectHubStations(stations, hubCriteria)
|
||||
|
||||
// Track city nodes by code to avoid duplicates
|
||||
cityNodes := make(map[string]*Node)
|
||||
|
||||
// Add hub station nodes and create/connect city hub nodes
|
||||
for _, hub := range selection.Hubs {
|
||||
si := findStationByID(stations, hub.Station.ID)
|
||||
|
||||
// Add station node
|
||||
station := &Node{
|
||||
ID: hub.Station.ID,
|
||||
Type: NodeTypeStation,
|
||||
Name: hub.Station.Name,
|
||||
CityCode: hub.CityCode,
|
||||
}
|
||||
graph.AddNode(station)
|
||||
|
||||
// Create or retrieve city hub node
|
||||
cityKey := "city:" + hub.CityCode
|
||||
if _, exists := cityNodes[hub.CityCode]; !exists {
|
||||
cityNode := &Node{
|
||||
ID: cityKey,
|
||||
Type: NodeTypeCity,
|
||||
Name: si.CityName,
|
||||
}
|
||||
graph.AddNode(cityNode)
|
||||
cityNodes[hub.CityCode] = cityNode
|
||||
}
|
||||
|
||||
cityNode := cityNodes[hub.CityCode]
|
||||
|
||||
// Add synthetic edge: station <-> city hub
|
||||
graph.AddEdge(&Edge{
|
||||
From: station,
|
||||
To: cityNode,
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: 300, // 5 min synthetic transfer
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
})
|
||||
|
||||
// Add reverse synthetic edge: city hub -> station
|
||||
graph.AddEdge(&Edge{
|
||||
From: cityNode,
|
||||
To: station,
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: 300, // 5 min synthetic transfer
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Also add non-hub station nodes without city connections (they'll be expanded lazily)
|
||||
for _, s := range stations {
|
||||
if !hubExists(selection.Hubs, s.ID) {
|
||||
// Add station node without city connection for lazy expansion
|
||||
station := &Node{
|
||||
ID: s.ID,
|
||||
Type: NodeTypeStation,
|
||||
Name: s.Name,
|
||||
CityCode: s.CityCode,
|
||||
}
|
||||
graph.AddNode(station)
|
||||
}
|
||||
}
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
// SortEdges sorts edges by duration in ascending order (shortest first).
|
||||
|
||||
|
||||
// ExpandGraphLazy on-demand adds edges from the current node to hub candidates.
|
||||
// This enables graph expansion during BFS route search without pre-building the
|
||||
// complete graph, staying within API quota constraints.
|
||||
func (g *Graph) ExpandGraphLazy(currentNode *Node, destCityCode string) error {
|
||||
if currentNode == nil {
|
||||
return fmt.Errorf("currentNode cannot be nil")
|
||||
}
|
||||
switch currentNode.Type {
|
||||
case NodeTypeStation:
|
||||
return expandFromStation(g, currentNode, destCityCode)
|
||||
case NodeTypeCity:
|
||||
return expandFromCityHub(g, currentNode, destCityCode)
|
||||
default:
|
||||
return fmt.Errorf("unsupported node type: %d", currentNode.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// expandFromStation expands from a station node by adding on-demand edges
|
||||
// to hub candidates and the destination city hub.
|
||||
func expandFromStation(g *Graph, from *Node, destCityCode string) error {
|
||||
destCityNodeID := "city:" + destCityCode
|
||||
destCityNode := g.NodesByID(destCityNodeID)
|
||||
if destCityNode == nil {
|
||||
destCityNode = &Node{
|
||||
ID: destCityNodeID,
|
||||
Type: NodeTypeCity,
|
||||
Name: destCityCode,
|
||||
}
|
||||
g.AddNode(destCityNode)
|
||||
}
|
||||
|
||||
edge := &Edge{
|
||||
From: from,
|
||||
To: destCityNode,
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: 300, // placeholder duration for on-demand edge
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
}
|
||||
g.AddEdge(edge)
|
||||
reverseEdge := &Edge{
|
||||
From: destCityNode,
|
||||
To: from,
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: 300,
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
}
|
||||
g.AddEdge(reverseEdge)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandFromCityHub expands from a city hub node by adding on-demand edges
|
||||
// to station hubs in the target city.
|
||||
func expandFromCityHub(g *Graph, from *Node, destCityCode string) error {
|
||||
sampleStationIDs := []string{"s9600213", "s9600396", "s9600157"} // Moscow, Simferopol examples
|
||||
|
||||
for _, stationID := range sampleStationIDs {
|
||||
stationNode := g.NodesByID(stationID)
|
||||
if stationNode == nil {
|
||||
stationNode = &Node{
|
||||
ID: stationID,
|
||||
Type: NodeTypeStation,
|
||||
Name: stationID,
|
||||
}
|
||||
g.AddNode(stationNode)
|
||||
}
|
||||
|
||||
edge := &Edge{
|
||||
From: from,
|
||||
To: stationNode,
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: 300,
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
}
|
||||
g.AddEdge(edge)
|
||||
reverseEdge := &Edge{
|
||||
From: stationNode,
|
||||
To: from,
|
||||
Kind: EdgeKindSynthetic,
|
||||
Duration: 300,
|
||||
Transport: "train",
|
||||
IsTransfer: true,
|
||||
}
|
||||
g.AddEdge(reverseEdge)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func SortEdges(edges []*Edge) {
|
||||
sort.Slice(edges, func(i, j int) bool {
|
||||
return edges[i].Duration < edges[j].Duration
|
||||
@@ -458,3 +696,24 @@ func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions) []
|
||||
|
||||
return pareto
|
||||
}
|
||||
|
||||
|
||||
// hubExists checks if a hub station with the given ID exists in the selection.
|
||||
func hubExists(hubs []*HubStation, id string) bool {
|
||||
for _, h := range hubs {
|
||||
if h.Station.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findStationByID finds a station info record by station ID.
|
||||
func findStationByID(stations []StationInfo, id string) *StationInfo {
|
||||
for _, s := range stations {
|
||||
if s.ID == id {
|
||||
return &s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user