Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
build-and-smoke:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y openmpi-bin libopenmpi-dev python3 python3-matplotlib

- name: Build
run: make

- name: Smoke test (deterministic)
run: |
mpirun --allow-run-as-root -np 2 bin/tsp_solver data/berlin52.tsp --replicas 4 --steps 10 --deterministic --swap-interval 2

- name: Run plotting script
run: |
python3 script/plot_results.py --solution solution.txt --route my_route.txt --tsp data/berlin52.tsp --output figures/route_comparison_python.png
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
*.out
*.exe
tsp_solver
bin/tsp_solver

# Generated benchmark output
*.log
scaling_results.csv
solution.txt
my_route.txt
figures/route_comparison_python.png

# Editors and operating systems
.vscode/
Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ Before opening a pull request:
2. Keep changes limited to one topic.
3. Compile the solver with `make`.
4. Run at least one Berlin52 case and check that the output is sensible.
5. Note the compiler, MPI implementation, and process count used for testing.
5. Run the local CI smoke test:
- `mpirun --allow-run-as-root -np 2 bin/tsp_solver data/berlin52.tsp --replicas 4 --steps 10 --deterministic --swap-interval 2`
- `python3 script/plot_results.py --solution solution.txt --route my_route.txt --tsp data/berlin52.tsp --output figures/route_comparison_python.png`
6. Mention whether CI passed (GitHub Actions workflow in `.github/workflows/ci.yml`).
7. Note the compiler, MPI implementation, and process count used for testing.

For C and MPI changes, use four-space indentation, free allocated memory, and add comments only where the communication or algorithmic logic is not obvious.

Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
CC = mpicc
CFLAGS = -O3 -std=c99 -Wall -Wextra
LDLIBS = -lm
TARGET = tsp_solver
BIN_DIR := bin
TARGET = $(BIN_DIR)/tsp_solver
SOURCE = script/tsp_mpi.c

.PHONY: all clean
.PHONY: all clean run-example

all: $(TARGET)

$(TARGET): $(SOURCE)
mkdir -p $(BIN_DIR)
$(CC) $(CFLAGS) $(SOURCE) -o $(TARGET) $(LDLIBS)

run-example: $(TARGET)
@echo "Example:"
@echo " mpirun --allow-run-as-root -np 2 $(TARGET) data/berlin52.tsp --replicas 4 --steps 10 --deterministic --swap-interval 2"

clean:
rm -f $(TARGET) *.o
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The project uses parallel tempering to study how a stochastic optimization metho
- Deadlock-free communication between neighboring MPI ranks
- Strong-scaling measurements across several process counts
- Octave scripts for plotting runtime, efficiency, and route quality
- Python plotting script (`script/plot_results.py`) for route visualization without MATLAB/Octave

## How the solver works

Expand Down Expand Up @@ -72,35 +73,84 @@ For the tested Berlin52 configuration, the parallel version reached a reported s
<img src="figures/scaling_quality_bar.png" width="620" alt="Solution quality across process counts">
</p>

## Running the code
## Getting started

On Ubuntu or Debian, install the main dependencies with:

```bash
sudo apt install build-essential openmpi-bin libopenmpi-dev octave
sudo apt install build-essential openmpi-bin libopenmpi-dev python3 python3-matplotlib octave
```

Build and run the included Berlin52 case:
Build the solver:

```bash
git clone https://github.com/Kandil2001/Distributed-TSP-Solver.git
cd Distributed-TSP-Solver
make
mpirun -np 4 ./tsp_solver data/berlin52.tsp
```

The cluster scaling study can be submitted with:
The binary is generated at `bin/tsp_solver`.

## How to run

Run with defaults (`data/berlin52.tsp`, 24 replicas, 260000 steps):

```bash
mpirun --allow-run-as-root -np 4 bin/tsp_solver
```

Run with explicit options (short deterministic smoke run):

```bash
mpirun --allow-run-as-root -np 2 bin/tsp_solver data/berlin52.tsp \
--replicas 4 --steps 10 --swap-interval 2 --deterministic
```

Available CLI flags:

- `--replicas N`
- `--steps N`
- `--swap-interval N`
- `--initial-temp X`
- `--temp-decay X`
- `--seed N`
- `--deterministic`
- `--help`

Important: replicas must divide evenly across MPI processes (`replicas % np == 0`).

The solver writes:

- `solution.txt` (best distance)
- `my_route.txt` (city index route for plotting)

Plot results without MATLAB:

```bash
python3 script/plot_results.py --solution solution.txt --route my_route.txt --tsp data/berlin52.tsp
```

The cluster scaling study can still be submitted with:

```bash
sbatch script/scaling_test.sh
```

For local CI-equivalent smoke testing:

```bash
make
mpirun --allow-run-as-root -np 2 bin/tsp_solver data/berlin52.tsp --replicas 4 --steps 10 --deterministic --swap-interval 2
python3 script/plot_results.py --solution solution.txt --route my_route.txt --tsp data/berlin52.tsp --output figures/route_comparison_python.png
```

## Repository structure

```text
script/tsp_mpi.c MPI solver
script/scaling_test.sh cluster scaling study
script/plot_*.m Octave plotting scripts
script/plot_results.py Python plotting script (no MATLAB required)
data/berlin52.tsp included benchmark case
figures/ selected published plots
Makefile local build configuration
Expand Down
128 changes: 128 additions & 0 deletions script/plot_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import argparse
import pathlib
import sys

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt


def read_solution(path: pathlib.Path) -> float | None:
if not path.exists():
return None
text = path.read_text(encoding="utf-8").strip()
if not text:
return None
try:
return float(text.splitlines()[0].strip())
except ValueError:
return None


def read_route(path: pathlib.Path) -> list[int]:
route: list[int] = []
for line in path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped:
continue
route.append(int(stripped))
return route


def read_coords(tsplib_path: pathlib.Path) -> list[tuple[float, float]]:
coords: list[tuple[float, float]] = []
in_section = False
for line in tsplib_path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped:
continue
if stripped.startswith("NODE_COORD_SECTION"):
in_section = True
continue
if stripped.startswith("EOF"):
break
if in_section:
parts = stripped.split()
if len(parts) >= 3:
coords.append((float(parts[1]), float(parts[2])))
return coords


def close_route(route: list[int]) -> list[int]:
if not route:
return route
if route[0] == route[-1]:
return route
return route + [route[0]]


def main() -> int:
parser = argparse.ArgumentParser(description="Plot TSP route output using matplotlib.")
parser.add_argument("--solution", default="solution.txt", help="Path to solution.txt")
parser.add_argument("--route", default="my_route.txt", help="Path to my_route.txt")
parser.add_argument("--tsp", default="data/berlin52.tsp", help="Optional TSPLIB file")
parser.add_argument("--output", default="figures/route_comparison_python.png", help="Output image path")
args = parser.parse_args()

route_path = pathlib.Path(args.route)
if not route_path.exists():
print(f"Error: route file not found: {route_path}", file=sys.stderr)
return 1

tsp_path = pathlib.Path(args.tsp)
if not tsp_path.exists():
print(f"Error: TSPLIB file not found: {tsp_path}", file=sys.stderr)
return 1

route = read_route(route_path)
coords = read_coords(tsp_path)

if not route:
print("Error: route file is empty.", file=sys.stderr)
return 1
if not coords:
print("Error: could not parse coordinates from TSPLIB file.", file=sys.stderr)
return 1

if max(route) >= len(coords) or min(route) < 0:
print("Error: route indices are out of bounds for TSPLIB coordinates.", file=sys.stderr)
return 1

route_closed = close_route(route)
ref_route = list(range(len(coords)))
ref_closed = close_route(ref_route)

x_ref = [coords[i][0] for i in ref_closed]
y_ref = [coords[i][1] for i in ref_closed]
x_sol = [coords[i][0] for i in route_closed]
y_sol = [coords[i][1] for i in route_closed]

best_dist = read_solution(pathlib.Path(args.solution))

fig, axes = plt.subplots(1, 2, figsize=(12, 5), constrained_layout=True)

axes[0].plot(x_ref, y_ref, "o-", linewidth=1.0, markersize=3)
axes[0].set_title("Reference order route")
axes[0].set_aspect("equal", adjustable="box")

axes[1].plot(x_sol, y_sol, "o-", linewidth=1.0, markersize=3, color="tab:orange")
if best_dist is None:
axes[1].set_title("Optimized route")
else:
axes[1].set_title(f"Optimized route (distance={best_dist:.2f})")
axes[1].set_aspect("equal", adjustable="box")

for ax in axes:
ax.set_xlabel("x")
ax.set_ylabel("y")

out_path = pathlib.Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(out_path, dpi=160)
print(f"Saved plot: {out_path}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading