Use matplotlib for lab6 visuals and expand report

This commit is contained in:
Artem
2025-11-21 17:29:02 +03:00
parent 9f591dadda
commit 93ab829cff
3 changed files with 79 additions and 113 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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}