Use matplotlib for lab6 visuals and expand report
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
# Лабораторная работа 6
|
# Лабораторная работа 6
|
||||||
|
|
||||||
## Как получить изображения и данные
|
## Как получить изображения и данные
|
||||||
1. Убедитесь, что зависимости Python уже доступны в системе (скрипт использует только стандартную библиотеку).
|
1. Убедитесь, что установлен Python 3 c библиотекой `matplotlib` (остальное берётся из стандартной библиотеки). При необходимости установите её командой:
|
||||||
|
```bash
|
||||||
|
pip install matplotlib
|
||||||
|
```
|
||||||
2. Запустите оптимизатор и генерацию графиков:
|
2. Запустите оптимизатор и генерацию графиков:
|
||||||
```bash
|
```bash
|
||||||
python lab6/main.py
|
python lab6/main.py
|
||||||
|
|||||||
126
lab6/aco.py
126
lab6/aco.py
@@ -1,10 +1,10 @@
|
|||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import struct
|
|
||||||
import zlib
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Sequence, Tuple
|
from typing import List, Sequence, Tuple
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
City = Tuple[float, float]
|
City = Tuple[float, float]
|
||||||
Tour = List[int]
|
Tour = List[int]
|
||||||
|
|
||||||
@@ -23,116 +23,38 @@ def build_distance_matrix(cities: Sequence[City]) -> list[list[float]]:
|
|||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
|
|
||||||
def _write_png(filename: str, pixels: list[list[tuple[int, int, int]]]) -> None:
|
|
||||||
height = len(pixels)
|
|
||||||
width = len(pixels[0]) if height else 0
|
|
||||||
|
|
||||||
def chunk(chunk_type: bytes, data: bytes) -> bytes:
|
|
||||||
return (
|
|
||||||
struct.pack(">I", len(data))
|
|
||||||
+ chunk_type
|
|
||||||
+ data
|
|
||||||
+ struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
|
|
||||||
)
|
|
||||||
|
|
||||||
raw = b"".join(b"\x00" + bytes([c for px in row for c in px]) for row in pixels)
|
|
||||||
png = b"\x89PNG\r\n\x1a\n"
|
|
||||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
|
||||||
png += chunk(b"IHDR", ihdr)
|
|
||||||
png += chunk(b"IDAT", zlib.compress(raw, 9))
|
|
||||||
png += chunk(b"IEND", b"")
|
|
||||||
|
|
||||||
with open(filename, "wb") as f:
|
|
||||||
f.write(png)
|
|
||||||
|
|
||||||
|
|
||||||
def _scale_points(points: Sequence[tuple[float, float]], size: int = 800, margin: int = 20):
|
|
||||||
xs = [p[0] for p in points]
|
|
||||||
ys = [p[1] for p in points]
|
|
||||||
min_x, max_x = min(xs), max(xs)
|
|
||||||
min_y, max_y = min(ys), max(ys)
|
|
||||||
scale_x = (size - 2 * margin) / (max_x - min_x + 1e-9)
|
|
||||||
scale_y = (size - 2 * margin) / (max_y - min_y + 1e-9)
|
|
||||||
return [
|
|
||||||
(
|
|
||||||
int((x - min_x) * scale_x + margin),
|
|
||||||
int((y - min_y) * scale_y + margin),
|
|
||||||
)
|
|
||||||
for x, y in points
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _draw_line(pixels: list[list[tuple[int, int, int]]], p1: tuple[int, int], p2: tuple[int, int], color: tuple[int, int, int]):
|
|
||||||
x1, y1 = p1
|
|
||||||
x2, y2 = p2
|
|
||||||
dx = abs(x2 - x1)
|
|
||||||
dy = -abs(y2 - y1)
|
|
||||||
sx = 1 if x1 < x2 else -1
|
|
||||||
sy = 1 if y1 < y2 else -1
|
|
||||||
err = dx + dy
|
|
||||||
while True:
|
|
||||||
if 0 <= x1 < len(pixels[0]) and 0 <= y1 < len(pixels):
|
|
||||||
pixels[y1][x1] = color
|
|
||||||
if x1 == x2 and y1 == y2:
|
|
||||||
break
|
|
||||||
e2 = 2 * err
|
|
||||||
if e2 >= dy:
|
|
||||||
err += dy
|
|
||||||
x1 += sx
|
|
||||||
if e2 <= dx:
|
|
||||||
err += dx
|
|
||||||
y1 += sy
|
|
||||||
|
|
||||||
|
|
||||||
def _draw_circle(pixels: list[list[tuple[int, int, int]]], center: tuple[int, int], radius: int, color: tuple[int, int, int]):
|
|
||||||
cx, cy = center
|
|
||||||
for y in range(cy - radius, cy + radius + 1):
|
|
||||||
for x in range(cx - radius, cx + radius + 1):
|
|
||||||
if 0 <= x < len(pixels[0]) and 0 <= y < len(pixels):
|
|
||||||
if (x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2:
|
|
||||||
pixels[y][x] = color
|
|
||||||
|
|
||||||
|
|
||||||
def plot_tour(cities: Sequence[City], tour: Sequence[int], save_path: str) -> None:
|
def plot_tour(cities: Sequence[City], tour: Sequence[int], save_path: str) -> None:
|
||||||
ordered = [cities[i] for i in tour] + [cities[tour[0]]]
|
ordered = [cities[i] for i in tour] + [cities[tour[0]]]
|
||||||
points = _scale_points(ordered)
|
xs, ys = zip(*ordered)
|
||||||
width = height = 820
|
|
||||||
pixels = [[(255, 255, 255) for _ in range(width)] for _ in range(height)]
|
|
||||||
|
|
||||||
for i in range(len(points) - 1):
|
fig, ax = plt.subplots(figsize=(7, 7))
|
||||||
_draw_line(pixels, points[i], points[i + 1], (0, 120, 200))
|
ax.plot(xs, ys, "-o", color="#1f77b4", markersize=4, linewidth=1.5)
|
||||||
|
city_xs, city_ys = zip(*cities)
|
||||||
|
ax.scatter(city_xs, city_ys, s=18, color="#d62728", zorder=5)
|
||||||
|
|
||||||
# draw cities
|
ax.set_xlabel("X")
|
||||||
city_points = _scale_points(cities)
|
ax.set_ylabel("Y")
|
||||||
for p in city_points:
|
ax.set_title("Маршрут тура")
|
||||||
_draw_circle(pixels, p, 4, (200, 50, 50))
|
ax.set_aspect("equal", adjustable="box")
|
||||||
|
ax.grid(True, linestyle="--", alpha=0.3)
|
||||||
_write_png(save_path, pixels)
|
fig.tight_layout()
|
||||||
|
fig.savefig(save_path, dpi=220)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
def plot_history(best_lengths: Sequence[float], save_path: str) -> None:
|
def plot_history(best_lengths: Sequence[float], save_path: str) -> None:
|
||||||
if not best_lengths:
|
if not best_lengths:
|
||||||
return
|
return
|
||||||
|
|
||||||
width, height, margin = 820, 400, 20
|
fig, ax = plt.subplots(figsize=(8, 3.8))
|
||||||
pixels = [[(255, 255, 255) for _ in range(width)] for _ in range(height)]
|
ax.plot(best_lengths, color="#111111", linewidth=1.4)
|
||||||
|
ax.set_xlabel("Итерация")
|
||||||
n = len(best_lengths)
|
ax.set_ylabel("Длина лучшего тура")
|
||||||
min_len, max_len = min(best_lengths), max(best_lengths)
|
ax.set_title("Сходимость ACO")
|
||||||
span = max_len - min_len if max_len != min_len else 1
|
ax.grid(True, linestyle="--", alpha=0.4)
|
||||||
|
fig.tight_layout()
|
||||||
def to_point(idx: int, value: float) -> tuple[int, int]:
|
fig.savefig(save_path, dpi=220)
|
||||||
x = margin + int((width - 2 * margin) * idx / max(1, n - 1))
|
plt.close(fig)
|
||||||
y = height - margin - int((height - 2 * margin) * (value - min_len) / span)
|
|
||||||
return x, y
|
|
||||||
|
|
||||||
prev = to_point(0, best_lengths[0])
|
|
||||||
for i, v in enumerate(best_lengths[1:], start=1):
|
|
||||||
cur = to_point(i, v)
|
|
||||||
_draw_line(pixels, prev, cur, (30, 30, 30))
|
|
||||||
prev = cur
|
|
||||||
|
|
||||||
_write_png(save_path, pixels)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -256,16 +256,57 @@
|
|||||||
\newpage
|
\newpage
|
||||||
\section{Особенности реализации}
|
\section{Особенности реализации}
|
||||||
|
|
||||||
В рамках шестой лабораторной работы реализован простой муравьиный алгоритм для решения задачи коммивояжёра. Алгоритм оформлен в модуле \texttt{aco.py} и состоит из следующих компонентов:
|
Код решения собран в модуле \texttt{lab6/aco.py}. Ниже приведены ключевые элементы реализации с небольшими листингами (язык Python) и пояснениями.
|
||||||
\begin{itemize}
|
|
||||||
\item \textbf{Структуры данных}: конфигурация \texttt{ACOConfig} (число муравьёв, количество итераций, параметры $\alpha$, $\beta$, $\rho$ и $q$) и результат \texttt{ACOResult} (лучший тур, его длина и история улучшений).
|
|
||||||
\item \textbf{Матрицы расстояний и феромона}: расстояния между городами предвычисляются один раз; феромон хранится в виде симметричной матрицы и инициализируется единицами с нулями на диагонали.
|
|
||||||
\item \textbf{Построение тура}: каждый муравей стартует в случайном городе и последовательно добавляет вершины. Выбор следующего города происходит по вероятности, пропорциональной $\tau^\alpha \cdot (1/d)^\beta$, где $\tau$ — феромон на ребре, $d$ — расстояние между городами.
|
|
||||||
\item \textbf{Обновление феромона}: после прохода всех муравьёв выполняется испарение $\tau \leftarrow (1-\rho)\tau$ и добавление феромона $q/L$ на рёбра их маршрутов, где $L$ — длина тура.
|
|
||||||
\item \textbf{Визуализация}: для отчёта сгенерированы PNG-файлы. График маршрута рисуется посредством собственного минимального генератора PNG (без сторонних библиотек), который строит линии по методу Брезенхема и сохраняет изображение в папку \texttt{lab6/report/img}.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
Для загрузки координат использован тот же код, что и в лабораторной работе №3: исходные точки читаются из \texttt{lab3/data.txt}, где в файле содержатся 38 уникальных городов.
|
\subsection{Структуры данных и инициализация}
|
||||||
|
Конфигурация алгоритма и структура результата оформлены через \texttt{dataclass}; в конфиге задаются параметры $\alpha$, $\beta$, $\rho$, $q$, число муравьёв и итераций, а также зерно генератора случайных чисел:
|
||||||
|
\begin{lstlisting}[language=Python]
|
||||||
|
@dataclass
|
||||||
|
class ACOConfig:
|
||||||
|
cities: Sequence[City]
|
||||||
|
n_ants: int
|
||||||
|
n_iterations: int
|
||||||
|
alpha: float = 1.0
|
||||||
|
beta: float = 5.0
|
||||||
|
rho: float = 0.5
|
||||||
|
q: float = 1.0
|
||||||
|
seed: int | None = None
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
При создании \texttt{AntColonyOptimizer} матрица расстояний вычисляется один раз, а феромон инициализируется единицами (с нулями на диагонали), чтобы не допускать самопереходов.
|
||||||
|
|
||||||
|
\subsection{Построение и оценка тура}
|
||||||
|
Каждый муравей стартует в случайном городе и расширяет маршрут, используя вероятностный выбор следующей вершины, где вес ребра определяется как $\tau^\alpha \cdot (1/d)^\beta$:
|
||||||
|
\begin{lstlisting}[language=Python]
|
||||||
|
def _choose_next_city(self, current: int, unvisited: set[int]) -> int:
|
||||||
|
candidates = list(unvisited)
|
||||||
|
weights = []
|
||||||
|
for nxt in candidates:
|
||||||
|
tau = self.pheromone[current][nxt] ** self.config.alpha
|
||||||
|
eta = (1.0 / (self.dist_matrix[current][nxt] + 1e-12)) ** self.config.beta
|
||||||
|
weights.append(tau * eta)
|
||||||
|
return random.choices(candidates, weights=weights, k=1)[0]
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
Длина тура вычисляется как сумма евклидовых расстояний между последовательными городами, включая возврат в исходную точку.
|
||||||
|
|
||||||
|
\subsection{Обновление феромона}
|
||||||
|
После завершения итерации выполняется испарение и добавление феромона $q/L$ на рёбра маршрутов всех муравьёв. Короткие маршруты оставляют более сильный след и начинают доминировать в вероятностном выборе:
|
||||||
|
\begin{lstlisting}[language=Python]
|
||||||
|
for i in range(len(self.pheromone)):
|
||||||
|
for j in range(len(self.pheromone)):
|
||||||
|
self.pheromone[i][j] *= 1 - self.config.rho
|
||||||
|
|
||||||
|
for tour, length in zip(tours, lengths):
|
||||||
|
deposit = self.config.q / length
|
||||||
|
for i in range(len(tour)):
|
||||||
|
a, b = tour[i], tour[(i + 1) % len(tour)]
|
||||||
|
self.pheromone[a][b] += deposit
|
||||||
|
self.pheromone[b][a] += deposit
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsection{Загрузка данных и визуализация}
|
||||||
|
Координаты городов считываются из \texttt{lab3/data.txt}; в файле содержатся 38 уникальных точек. Для визуализации используется \texttt{matplotlib}, что позволяет сохранить исходную ориентацию системы координат (ось $Y$ направлена вверх) и избежать инверсии рисунка. Функция \texttt{plot\_tour} строит ломаную линию обхода, подсвечивает вершины и сохраняет результат в \texttt{lab6/report/img}. График сходимости \texttt{plot\_history} отображает изменение лучшей длины тура по итерациям с сеткой и подписями осей.
|
||||||
|
|
||||||
\newpage
|
\newpage
|
||||||
\section{Результаты работы}
|
\section{Результаты работы}
|
||||||
@@ -308,7 +349,7 @@
|
|||||||
В ходе шестой лабораторной работы выполнена реализация простого муравьиного алгоритма для задачи коммивояжёра:
|
В ходе шестой лабораторной работы выполнена реализация простого муравьиного алгоритма для задачи коммивояжёра:
|
||||||
|
|
||||||
\begin{enumerate}
|
\begin{enumerate}
|
||||||
\item Разработан модуль \texttt{aco.py} с конфигурацией алгоритма, построением туров, обновлением феромона и собственными средствами визуализации без сторонних библиотек.
|
\item Разработан модуль \texttt{aco.py} с конфигурацией алгоритма, построением туров, обновлением феромона и визуализацией результатов с помощью \texttt{matplotlib}.
|
||||||
\item Проведён численный эксперимент на данных из варианта 18 (38 городов Джибути); подобраны параметры $\alpha=1{,}2$, $\beta=5$, $\rho=0{,}5$, 50 муравьёв, 400 итераций.
|
\item Проведён численный эксперимент на данных из варианта 18 (38 городов Джибути); подобраны параметры $\alpha=1{,}2$, $\beta=5$, $\rho=0{,}5$, 50 муравьёв, 400 итераций.
|
||||||
\item Получено приближённое решение длиной 6662{,}35, что всего на 0{,}05\% хуже известного оптимума 6659 и лучше результата, достигнутого генетическим алгоритмом из лабораторной работы №3.
|
\item Получено приближённое решение длиной 6662{,}35, что всего на 0{,}05\% хуже известного оптимума 6659 и лучше результата, достигнутого генетическим алгоритмом из лабораторной работы №3.
|
||||||
\end{enumerate}
|
\end{enumerate}
|
||||||
|
|||||||
Reference in New Issue
Block a user