Skip to content

phaphuang/HTS-RPR

Repository files navigation

HTS-RPR — Heuristic TSPTW Solver with POI Recommendation

A modular Python package for tourist route planning that combines an exact MIP-based route optimiser with a heuristic POI recommendation filter and a Genetic Algorithm (GA). The method is designed for the Orienteering Problem with Time Windows (OPTW) in urban temple tourism contexts (Chiang Mai, Thailand).


Overview

HTS-RPR solves the following problem:

Given a set of mandatory Points of Interest (P), a start location (S), an end location (E), a tour time window [T_start, T_end], and a pool of candidate POIs (C), find the route that maximises total reward (adjusted for Bayesian credibility and peak-time crowding) while satisfying all time-window constraints.

The pipeline runs in three stages:

[Algorithm 1]  MIP-based TSPTW  →  optimal mandatory route R*
      ↓
[Algorithm 2]  Heuristic POI Recommendation  →  ranked candidate list F
      ↓
[Algorithm 3]  Genetic Algorithm over P ∪ top-k(F)  →  final optimised route

Repository Structure

HTS-RPR/
├── data/
│   ├── combined_driving_time_nodes.json   # 64 nodes (temples, hubs, hotels) in Chiang Mai
│   └── combined_driving_time_edgelist.csv # Pairwise driving-time edges (seconds)
├── utils.py                  # Shared utilities (time conversion, Bayesian reward, feasibility)
├── mip_solver.py             # Algorithm 1 (MIP TSPTW) + Algorithm 2 (POI Recommendation)
├── ga_solver.py              # Genetic Algorithm solver (TSPTWGeneticAlgorithm)
├── integration.py            # End-to-end pipeline runner (run_hts_rpr)
├── evaluate_60_scenarios.py  # Paper benchmark: runs HTS-RPR on 60 mixed scenarios
├── results/                  # Auto-generated evaluation outputs (CSV / JSON / log)
├── requirements.txt
└── README.md

Dataset

Nodes — data/combined_driving_time_nodes.json

64 nodes in Chiang Mai city, grouped into three categories:

Category Count Description
temple 60 Buddhist temples and religious sites
hub 2 Bus terminal, airport
hotel 2 Accommodation starting/ending points

Each node entry:

{
  "0": {
    "name": "Watchediluang Varaviharn",
    "latitude": 18.7869,
    "longitude": 98.9865,
    "rating": 4.7,
    "total_ratings": 15370,
    "open": "05:00:00",
    "close": "22:30:00",
    "besttime": "05:00:00",
    "peaktime": "19:00:00",
    "category": "temple"
  }
}
Field Type Description
name str Place name
latitude, longitude float GPS coordinates
rating float Average Google Maps star rating
total_ratings int Number of reviews
open / close str HH:MM:SS Operating hours
besttime str HH:MM:SS Recommended visiting time
peaktime str HH:MM:SS Peak-crowding start time (1-hour window)
category str "temple" / "hub" / "hotel"

Edges — data/combined_driving_time_edgelist.csv

Pairwise driving-time edge list (no header):

source_id, target_id, weight_seconds
0, 1, 1679
0, 2, 198
...

Weights are in seconds. The package converts them to minutes internally.

Data status: The data/ files in this repository are identical to the source dataset in the parent research project and require no further update.


Installation

pip install -r requirements.txt

Dependencies:

Package Version Purpose
numpy ≥ 1.20 Matrix operations
pandas ≥ 1.3 Edge-list loading
pulp ≥ 2.6 MIP solver (CBC bundled)

No external API key or licence is required. PuLP ships with the CBC solver, which is used automatically.


Getting Started

Step 1 — Clone the repository

git clone https://github.com/<your-org>/HTS-RPR.git
cd HTS-RPR

Step 2 — Install dependencies

pip install -r requirements.txt

Step 3 — Run the pipeline

The scripts are standalone — run them directly with python. No API key, server, or external service is required; the bundled dataset under data/ is used by default.

Run the full HTS-RPR pipeline (Algorithm 1 → Algorithm 2 → GA) from the repository root:

python integration.py

This loads data/combined_driving_time_nodes.json and data/combined_driving_time_edgelist.csv, runs the full pipeline with the default scenario, and prints the optimised route:

============================================================
HTS-RPR RESULT
============================================================
Route:    [62, 33, 4, 2, 6, 9, 63, 44, 28, 62]
Total RP: 37.5701
Duration: 592.45 min
Fitness:  89.5105

You can also run just the MIP stages (Algorithm 1 + Algorithm 2) on their own:

python mip_solver.py

Customising the run

Every parameter is exposed as a command-line flag. View them all with --help:

python integration.py --help

Common examples:

# Different mandatory temples and a longer time window
python integration.py --selected-pois 2 9 4 7 --t-start 08:00:00 --t-end 18:00:00

# Faster GA run with verbose per-step logging suppressed
python integration.py --ga-time-limit 60 --ga-generations 50 --quiet

# Use your own dataset files
python integration.py --nodes path/to/nodes.json --edges path/to/edges.csv
Flag Default Description
--nodes data/combined_driving_time_nodes.json Nodes JSON path
--edges data/combined_driving_time_edgelist.csv Edge-list CSV path
--selected-pois 2 9 4 Mandatory POI node IDs (set P)
--start-node / --end-node 62 / 62 Start / end node (equal = round-trip)
--t-start / --t-end 07:00:00 / 17:00:00 Tour time window
--visit-duration 60 Minutes spent per POI
--alpha 1.0 Algorithm 2 time-buffer coefficient
--top-k 5 Recommendations passed to the GA
--ga-generations 100 Max GA generations
--ga-population 50 GA population size
--ga-mutation-pct 20 GA mutation probability (%)
--ga-time-limit 120.0 GA wall-clock limit (seconds)
--peak-reward-factor 0.5 Peak-time penalty factor
--mip-time-limit 300.0 MIP solver limit (seconds)
--quiet off Suppress per-step INFO logging

Script reference

Script Role Run directly?
integration.py Full pipeline runner (Algorithm 1 → 2 → GA) Yespython integration.py
mip_solver.py Algorithm 1 (MIP TSPTW) + Algorithm 2 (POI recommendation) Yespython mip_solver.py
evaluate_60_scenarios.py Paper benchmark over 60 mixed scenarios Yespython evaluate_60_scenarios.py
ga_solver.py Genetic Algorithm solver No — used by integration.py
utils.py Shared helpers (data loading, reward maths) No — used by the others

Embedding in your own code: the functions remain importable — from integration import run_hts_rpr — if you prefer to call the pipeline programmatically instead of via the command line.


Programmatic Usage (optional)

The command-line scripts above are the primary interface. If you prefer to embed HTS-RPR in your own Python code, the same functionality is available as importable functions and classes.

run_hts_rpr(...) — end-to-end pipeline

from integration import run_hts_rpr

result = run_hts_rpr(
    nodes_data,          # dict  – loaded JSON nodes
    driving_edges,       # pd.DataFrame – edge list
    selected_pois,       # list[int]  – mandatory POI IDs (set P)
    start_node,          # int  – start location ID
    end_node,            # int  – end location ID (= start_node for round-trip)
    t_start,             # str  – 'HH:MM:SS'
    t_end,               # str  – 'HH:MM:SS'
    candidate_pois=None, # list[int] | None (auto-discovers all non-P nodes)
    visit_duration=60,   # int  – minutes per POI
    alpha=1.0,           # float – Algorithm 2 time-buffer coefficient
    top_k=5,             # int  – recommendations passed to GA
    ga_generations=100,
    ga_population=50,
    ga_mutation_pct=20,
    ga_time_limit=300.0,
    peak_reward_factor=0.5,
    walking_edges=None,
    mip_time_limit=300.0,
)

Returns a dict with:

Key Type Description
mip_result dict Algorithm 1 output (route, schedule, total_time)
recommendations list[dict] Full ranked list from Algorithm 2
top_recommendations list[dict] Top-k subset used by the GA
ga_input dict Converted input fed to the GA
ga_route list[int] Best route found (node IDs)
ga_fitness float Fitness score: (Total_RP)³ / T
ga_details dict GA metrics (total_rp, total_time, execution_time, …)
success bool True when a feasible route was found

MIPRoutePlanner — use algorithms individually

from mip_solver import MIPRoutePlanner
from utils import load_nodes, load_edges

nodes_data    = load_nodes('data/combined_driving_time_nodes.json')
driving_edges = load_edges('data/combined_driving_time_edgelist.csv')

planner = MIPRoutePlanner(nodes_data, driving_edges)

# Algorithm 1 – mandatory route
mip_result = planner.algorithm1_tsptw_solver(
    selected_pois=[2, 9, 4],
    start_node=62, end_node=62,
    t_start='07:00:00', t_end='17:00:00',
    visit_duration=60,
)

# Algorithm 2 – candidate recommendations
recommendations = planner.algorithm2_poi_recommendation(
    optimal_result=mip_result,
    candidate_pois=[0, 1, 3, 5, 6, 7, 8],
    t_start='07:00:00', t_end='17:00:00',
    visit_duration=60,
    alpha=1.0,
)

for rec in recommendations[:5]:
    print(f"{rec['name']}: priority={rec['priority']:.3f}, ΔT={rec['delta_t']:.1f} min")

TSPTWGeneticAlgorithm — use the GA directly

from ga_solver import TSPTWGeneticAlgorithm
from integration import convert_mip_to_ga_input

ga_input = convert_mip_to_ga_input(
    optimal_result=mip_result,
    recommendations=recommendations[:5],
    planner=planner,
    selected_pois=[2, 9, 4],
    t_start='07:00:00', t_end='17:00:00',
    visit_duration=60,
)

solver = TSPTWGeneticAlgorithm(
    nodes            = ga_input['nodes'],
    distance_matrix  = ga_input['distance_matrix'],
    start_node       = ga_input['start_node'],
    end_node         = ga_input['end_node'],
    tour_start_time  = ga_input['t_start_min'],
    tour_end_time    = ga_input['t_end_min'],
    mandatory_nodes  = ga_input['mandatory_nodes'],
    peak_reward_factor = 0.5,
)

route, fitness, details = solver.solve(
    num_generations=100,
    sol_per_pop=50,
    mutation_percent_genes=20,
    time_limit=120.0,
)
print("Route:", route)
print("Fitness:", fitness)

Utility functions

from utils import (
    time_to_minutes,        # 'HH:MM:SS' → float (minutes from midnight)
    minutes_to_time_str,    # float → 'HH:MM:SS'
    build_distance_matrix,  # edge DataFrame → np.ndarray
    calculate_bayesian_params,  # nodes dict → (m, q)
    bayesian_reward,        # (r_i, c_i, m, q) → R_i
    peak_overlap,           # (arrival, departure, peaktime) → overlap minutes
    reward_penalty,         # full RP_i computation
    is_route_feasible,      # bool check
    build_ga_nodes,         # raw nodes_data → GA-format node dict
)

Algorithm Details

Reward Model

Bayesian average reward smooths raw ratings using the corpus-wide distribution:

$$R_i = \frac{r_i \cdot c_i + m \cdot q}{c_i + q}$$

where $r_i$ = raw rating, $c_i$ = review count, $m$ = weighted-average rating across all POIs, $q$ = 25th-percentile review count.

Peak-time reward penalty discounts reward when the visit overlaps the crowded peak hour:

$$RP_i = R_i \cdot \frac{s_i + (p - 1) \cdot o_i}{s_i}$$

where $s_i$ = service time, $o_i$ = overlap with the 1-hour peak window, $p$ = peak_reward_factor (0 < p ≤ 1).

GA fitness function:

$$\text{fitness} = \frac{\left(\sum_i RP_i\right)^3}{T}$$

where $T$ is total tour duration. Infeasible routes return $-\infty$.

Algorithm 1 — MIP TSPTW

Mixed Integer Programme using Miller–Tucker–Zemlin (MTZ) subtour elimination. Supports both round-trip (depot) and point-to-point scenarios. Falls back to CBC (PuLP bundled) when Gurobi is not available.

Algorithm 2 — Heuristic POI Recommendation

Two-stage filter:

  1. Global time feasibility: $\Delta T(c_j) \leq \alpha \cdot T_\text{remaining}$, where $T_\text{remaining}$ includes idle arc slack from the mandatory route.
  2. Per-edge time-window feasibility: simulates inserting $c_j$ at every position and propagates the shifted schedule forward.

Candidates passing both filters are ranked by $\text{priority} = R_i / \Delta T(c_j)$.

Genetic Algorithm

Parameter Default Description
num_generations 100 Max iterations
sol_per_pop 50 Population size
mutation_percent_genes 20 Mutation probability (%)
time_limit 300 s Wall-clock limit
peak_reward_factor 0.5 Peak-time penalty factor

Operators:

  • Selection: tournament selection (k = 3)
  • Crossover: order crossover with feasibility-guided reinsertion
  • Mutation: randomly applies one of swap, insert, or remove-and-add
  • Elitism: top 10 % of population carried forward unchanged
  • Stopping: stagnation (< 10⁻⁶ improvement for 5 consecutive generations) or time limit

Reproducing the Paper Evaluation

evaluate_60_scenarios.py reproduces the 60 mixed-scenario benchmark reported in the paper. It regenerates the exact scenario set (fixed random seed) and runs the full HTS-RPR pipeline on each one, then writes per-scenario metrics and a summary.

Run it directly from the repository root:

python evaluate_60_scenarios.py

No arguments are required — it loads the bundled dataset under data/ automatically.

What it does

  • Generates 60 scenarios (random_seed=42) across four categories, 15 each:
Category Difficulty Tour length Candidates
Tight Time Window tight 3–4 h 8
Many Candidates medium 5–6 h 10
Standard / Relaxed relaxed 7–8 h 6
Complex Constraints complex 7–8 h 7
  • Route types are rotated within each category: Temple→Temple, Hotel→Hotel, Hotel→Hub, Temple→Hotel.
  • Runs Algorithm 1 → Algorithm 2 → GA on each scenario and aggregates feasibility, fitness, reward (RP), travel time, POIs visited, and execution time.

Output

Results are saved to results/evaluate_60_scenarios/ with a run timestamp:

File Contents
evaluate_60_htsrpr_<timestamp>.csv One flat row per scenario (summary metrics)
evaluate_60_htsrpr_<timestamp>.json Full results, including per-stop schedules and top-k recommendations
evaluate_60_scenarios.log Full run log

A feasibility rate and per-difficulty / per-route-type performance summary are also printed to the console at the end of the run.

Tuning: evaluation parameters (GA generations, time limit, top_k, seed, etc.) live in the CONFIG dictionary near the top of evaluate_60_scenarios.py. The full run takes several minutes because it executes the MIP + GA pipeline 60 times.


Example Scenario

Running a full-day temple tour from Amora Hotel (node 62), visiting Wat Phra Singh (2), Wat Phra That Doi Suthep (9), and Wat Suan Dok (4) as mandatory stops, with automatic POI recommendation:

python integration.py --selected-pois 2 9 4 --start-node 62 --end-node 62 \
    --t-start 07:00:00 --t-end 17:00:00 --visit-duration 60 \
    --alpha 1.0 --top-k 5 --ga-generations 100 --ga-time-limit 120 \
    --peak-reward-factor 0.5

Example output (exact route varies run-to-run because the GA is stochastic):

============================================================
HTS-RPR RESULT
============================================================
Route:    [62, 33, 4, 2, 6, 9, 63, 44, 28, 62]
Total RP: 37.5701
Duration: 592.45 min
Fitness:  89.5105

Citation

If you use this code in academic work, please cite the original research:

@article{sokantika2026three,
  title={Three-Stage Optimization Algorithm for Sustainable Tourism Route Planning with Point-of-Interest Recommendation},
  author={Sokantika, Saronsad and Saksuriya, Payakorn and Ramasamy, Siva Shankar and Phaphuangwittayakul, Aniwat},
  journal={Applied System Innovation},
  year={2026},
  publisher={Multidisciplinary Digital Publishing Institute}
}

License

This repository is released for academic and research reuse. See LICENSE for details.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages