From b335daa881cb07bda397935e4df85ab7b1a23e31 Mon Sep 17 00:00:00 2001 From: anikinvd Date: Fri, 22 May 2026 18:01:05 +0000 Subject: [PATCH] [2] Add plots generator --- anikinvd/docs/data/2-nd-exercise/plots.py | 370 ++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 anikinvd/docs/data/2-nd-exercise/plots.py diff --git a/anikinvd/docs/data/2-nd-exercise/plots.py b/anikinvd/docs/data/2-nd-exercise/plots.py new file mode 100644 index 0000000..df92bcb --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/plots.py @@ -0,0 +1,370 @@ +import sys +import csv +from collections import deque +import heapq +import time +import matplotlib.pyplot as plt +import numpy as np + + +# ---------- Модель ---------- +class Node: + def __init__(self, x, y): + self.x = x + self.y = y + self.wall = False + self.start_flag = False + self.exit_flag = False + + @property + def is_wall(self): + return self.wall + + @is_wall.setter + def is_wall(self, val): + self.wall = val + + @property + def is_start(self): + return self.start_flag + + @is_start.setter + def is_start(self, val): + self.start_flag = val + + @property + def is_exit(self): + return self.exit_flag + + @is_exit.setter + def is_exit(self, val): + self.exit_flag = val + + def passable(self): + return not self.wall + + +class Grid: + def __init__(self, w, h): + self.w = w + self.h = h + self.cells = [[Node(x, y) for x in range(w)] for y in range(h)] + self.start_node = None + self.exit_node = None + + def get(self, x, y): + if 0 <= x < self.w and 0 <= y < self.h: + return self.cells[y][x] + return None + + def set_type(self, x, y, typ): + cell = self.get(x, y) + if not cell: + return + if typ == 'wall': + cell.is_wall = True + elif typ == 'start': + if self.start_node: + self.start_node.is_start = False + cell.is_start = True + cell.is_wall = False + self.start_node = cell + elif typ == 'exit': + if self.exit_node: + self.exit_node.is_exit = False + cell.is_exit = True + cell.is_wall = False + self.exit_node = cell + elif typ == 'path': + cell.is_wall = False + + def neighbors(self, node): + res = [] + dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)] + for dx, dy in dirs: + nx, ny = node.x + dx, node.y + dy + nb = self.get(nx, ny) + if nb and nb.passable(): + res.append(nb) + return res + + +class Loader: + def load(self, fname): + raise NotImplementedError + + +class TxtLoader(Loader): + def load(self, fname): + with open(fname, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + h = len(lines) + w = max(len(line) for line in lines) if h > 0 else 0 + start_cnt = 0 + exit_cnt = 0 + grid = Grid(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + grid.set_type(x, y, "wall") + elif ch == "S": + grid.set_type(x, y, "start") + start_cnt += 1 + elif ch == "E": + grid.set_type(x, y, "exit") + exit_cnt += 1 + else: + grid.set_type(x, y, 'path') + + if start_cnt != 1 or exit_cnt != 1: + raise ValueError(f"Bad maze: S={start_cnt}, E={exit_cnt}") + return grid + + +# ---------- Поисковые стратегии ---------- +class SearchAlgo: + def search(self, grid, start, goal): + raise NotImplementedError + + def _reconstruct(self, parent, start, goal): + path = [] + cur = goal + while cur: + path.append(cur) + cur = parent.get(cur) + path.reverse() + return path + + def visited_count(self): + return getattr(self, '_visited_num', 0) + + +class BFSAlgo(SearchAlgo): + def search(self, grid, start, goal): + q = deque([start]) + parent = {start: None} + seen = {start} + + while q: + cur = q.popleft() + if cur == goal: + self._visited_num = len(seen) + return self._reconstruct(parent, start, goal) + for nb in grid.neighbors(cur): + if nb not in seen: + seen.add(nb) + parent[nb] = cur + q.append(nb) + self._visited_num = len(seen) + return [] + + +class DFSAlgo(SearchAlgo): + def search(self, grid, start, goal): + stack = [start] + parent = {start: None} + seen = {start} + + while stack: + cur = stack.pop() + if cur == goal: + self._visited_num = len(seen) + return self._reconstruct(parent, start, goal) + for nb in grid.neighbors(cur): + if nb not in seen: + seen.add(nb) + parent[nb] = cur + stack.append(nb) + self._visited_num = len(seen) + return [] + + +class AStarAlgo(SearchAlgo): + def _h(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def search(self, grid, start, goal): + heap = [] + cnt = 0 + start_f = self._h(start, goal) + heapq.heappush(heap, (start_f, cnt, start)) + cnt += 1 + + parent = {} + g_score = {start: 0} + f_score = {start: start_f} + seen = set() + + while heap: + cur_f, _, cur = heapq.heappop(heap) + seen.add(cur) + if cur == goal: + self._visited_num = len(seen) + return self._reconstruct(parent, start, goal) + if cur_f > f_score.get(cur, float('inf')): + continue + for nb in grid.neighbors(cur): + tentative_g = g_score[cur] + 1 + if tentative_g < g_score.get(nb, float('inf')): + parent[nb] = cur + g_score[nb] = tentative_g + new_f = tentative_g + self._h(nb, goal) + f_score[nb] = new_f + heapq.heappush(heap, (new_f, cnt, nb)) + cnt += 1 + self._visited_num = len(seen) + return [] + + +class Solver: + def __init__(self, grid): + self.grid = grid + self.algo = None + + def set_algo(self, algo): + self.algo = algo + + def solve(self): + if not self.algo: + return None + t0 = time.perf_counter() + path = self.algo.search(self.grid, self.grid.start_node, self.grid.exit_node) + t1 = time.perf_counter() + elapsed = (t1 - t0) * 1000 + return { + 'time_ms': elapsed, + 'visited_cells': self.algo.visited_count(), + 'path_length': len(path) + } + + +def experiment(maze_file, algo, runs=5): + loader = TxtLoader() + grid = loader.load(maze_file) + total_t = 0.0 + total_v = 0 + total_l = 0 + for _ in range(runs): + s = Solver(grid) + s.set_algo(algo) + stats = s.solve() + if stats: + total_t += stats['time_ms'] + total_v += stats['visited_cells'] + total_l += stats['path_length'] + return { + 'time_ms': total_t / runs, + 'visited_cells': total_v / runs, + 'path_length': total_l / runs + } + + +def make_plots(results): + mazes = list(set(r['maze'] for r in results)) + algos = ['BFS', 'DFS', 'AStar'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + x = np.arange(len(mazes)) + width = 0.25 + + # Время + for i, algo in enumerate(algos): + times = [] + for m in mazes: + val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) + times.append(val) + axes[0].bar(x + i * width, times, width, label=algo) + axes[0].set_xlabel('Maze') + axes[0].set_ylabel('Time (ms)') + axes[0].set_title('Execution time') + axes[0].set_xticks(x + width) + axes[0].set_xticklabels(mazes, rotation=45, ha='right') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + # Посещённые клетки + for i, algo in enumerate(algos): + visited = [] + for m in mazes: + val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) + visited.append(val) + axes[1].bar(x + i * width, visited, width, label=algo) + axes[1].set_xlabel('Maze') + axes[1].set_ylabel('Visited cells') + axes[1].set_title('Visited cells comparison') + axes[1].set_xticks(x + width) + axes[1].set_xticklabels(mazes, rotation=45, ha='right') + axes[1].legend() + axes[1].grid(True, alpha=0.3) + + # Длина пути + for i, algo in enumerate(algos): + lengths = [] + for m in mazes: + val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) + lengths.append(val) + axes[2].bar(x + i * width, lengths, width, label=algo) + axes[2].set_xlabel('Maze') + axes[2].set_ylabel('Path length') + axes[2].set_title('Path length comparison') + axes[2].set_xticks(x + width) + axes[2].set_xticklabels(mazes, rotation=45, ha='right') + axes[2].legend() + axes[2].grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('performance_plot.png', dpi=150, bbox_inches='tight') + plt.show() + + +if __name__ == "__main__": + test_mazes = [ + ("maze1.txt", "Small 10x6"), + ("maze10x10.txt", "Medium 10x10"), + ("maze20x20.txt", "Large 20x20"), + ("maze_empty.txt", "Empty 15x15"), + ("maze_no_exit.txt", "No exit 10x10") + ] + algorithms = [ + ("BFS", BFSAlgo()), + ("DFS", DFSAlgo()), + ("AStar", AStarAlgo()) + ] + + results = [] + for fname, name in test_mazes: + print(f"Benchmarking {name}...") + for algo_name, algo in algorithms: + try: + stat = experiment(fname, algo, runs=3) + results.append({ + 'maze': name, + 'strategy': algo_name, + 'time_ms': stat['time_ms'], + 'visited_cells': stat['visited_cells'], + 'path_length': stat['path_length'] + }) + print(f" {algo_name}: time={stat['time_ms']:.3f}ms, visited={stat['visited_cells']:.0f}, length={stat['path_length']:.0f}") + except Exception as e: + print(f" {algo_name}: failed - {e}") + results.append({ + 'maze': name, + 'strategy': algo_name, + 'time_ms': -1, + 'visited_cells': -1, + 'path_length': -1 + }) + + valid = [r for r in results if r['time_ms'] >= 0] + + with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length']) + writer.writeheader() + writer.writerows(valid) + + if valid: + make_plots(valid) + + print("\nData saved to experiment_data.csv") + print("Plot saved to performance_plot.png") \ No newline at end of file