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).
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
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
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" |
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.
pip install -r requirements.txtDependencies:
| 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.
git clone https://github.com/<your-org>/HTS-RPR.git
cd HTS-RPRpip install -r requirements.txtThe 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.pyThis 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.pyEvery parameter is exposed as a command-line flag. View them all with --help:
python integration.py --helpCommon 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 | Role | Run directly? |
|---|---|---|
integration.py |
Full pipeline runner (Algorithm 1 → 2 → GA) | Yes — python integration.py |
mip_solver.py |
Algorithm 1 (MIP TSPTW) + Algorithm 2 (POI recommendation) | Yes — python mip_solver.py |
evaluate_60_scenarios.py |
Paper benchmark over 60 mixed scenarios | Yes — python 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.
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.
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 |
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")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)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
)Bayesian average reward smooths raw ratings using the corpus-wide distribution:
where
Peak-time reward penalty discounts reward when the visit overlaps the crowded peak hour:
where peak_reward_factor (0 < p ≤ 1).
GA fitness function:
where
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.
Two-stage filter:
-
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. -
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
| 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
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.pyNo arguments are required — it loads the bundled dataset under data/ automatically.
- 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.
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 theCONFIGdictionary near the top ofevaluate_60_scenarios.py. The full run takes several minutes because it executes the MIP + GA pipeline 60 times.
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.5Example 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
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}
}This repository is released for academic and research reuse. See LICENSE for details.