diff --git a/lab2/csv_to_tex.py b/lab2/csv_to_tex.py index 0934903..8d19df8 100644 --- a/lab2/csv_to_tex.py +++ b/lab2/csv_to_tex.py @@ -12,12 +12,16 @@ - "—" для отсутствующих данных Выходной файл: tables.tex с готовым LaTeX кодом всех таблиц. -Лучший результат по времени выполнения в каждой таблице выделяется жирным. +Лучшие результаты по времени и фитнесу выделяются жирным (и цветом, если задан HIGHLIGHT_COLOR). """ import re from pathlib import Path +# Настройка цвета для выделения лучших результатов +# None - только жирным, строка (например "magenta") - жирным и цветом +HIGHLIGHT_COLOR = "magenta" + def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]: """ @@ -53,7 +57,7 @@ def parse_csv_file(csv_path: str) -> tuple[str, list[list[str]]]: def extract_time_value(value: str) -> float | None: """ - Извлекает значение времени из строки формата "X.Y (Z)". + Извлекает значение времени из строки формата "X.Y (Z)" или "X.Y (Z) W.V". Args: value: Строка с результатом @@ -73,6 +77,29 @@ def extract_time_value(value: str) -> float | None: return None +def extract_fitness_value(value: str) -> float | None: + """ + Извлекает значение фитнеса из строки формата "X.Y (Z) W.V". + + Args: + value: Строка с результатом + + Returns: + Значение фитнеса как float или None если значение пустое + """ + value = value.strip() + if value == "—" or value == "" or value == "–": + return None + + # Ищем паттерн "число.число (число) число.число" + # Фитнес - это последнее число в строке + match = re.search(r"\)\s+(\d+\.?\d*)\s*$", value) + if match: + return float(match.group(1)) + + return None + + def find_best_time(data_rows: list[list[str]]) -> float | None: """ Находит минимальное время выполнения среди всех значений в таблице. @@ -95,13 +122,38 @@ def find_best_time(data_rows: list[list[str]]) -> float | None: return min_time -def format_value(value: str, best_time: float | None = None) -> str: +def find_best_fitness(data_rows: list[list[str]]) -> float | None: """ - Форматирует значение для LaTeX таблицы, выделяя лучший результат жирным. + Находит минимальное значение фитнеса среди всех значений в таблице. + + Args: + data_rows: Строки данных таблицы + + Returns: + Минимальное значение фитнеса или None если нет валидных значений + """ + min_fitness = None + + for row in data_rows: + for i in range(1, min(6, len(row))): # Пропускаем первую колонку (Pc) + fitness_value = extract_fitness_value(row[i]) + if fitness_value is not None: + if min_fitness is None or fitness_value < min_fitness: + min_fitness = fitness_value + + return min_fitness + + +def format_value( + value: str, best_time: float | None = None, best_fitness: float | None = None +) -> str: + """ + Форматирует значение для LaTeX таблицы, выделяя лучшие результаты жирным. Args: value: Строковое значение из CSV best_time: Лучшее время в таблице для сравнения + best_fitness: Лучший фитнес в таблице для сравнения Returns: Отформатированное значение для LaTeX @@ -110,16 +162,52 @@ def format_value(value: str, best_time: float | None = None) -> str: if value == "—" or value == "" or value == "–": return "—" - # Проверяем, является ли это лучшим результатом - current_time = extract_time_value(value) - if ( - current_time is not None - and best_time is not None - and abs(current_time - best_time) < 0.001 - ): - return f"\\textbf{{{value}}}" + # Проверяем есть ли фитнес в строке + fitness_match = re.search(r"(\d+\.?\d*)\s*\((\d+)\)\s+(\d+\.?\d*)\s*$", value) - return value + if fitness_match: + # Есть фитнес: "время (поколения) фитнес" + time_str = fitness_match.group(1) + generations_str = fitness_match.group(2) + fitness_str = fitness_match.group(3) + + current_time = float(time_str) + current_fitness = float(fitness_str) + + # Проверяем, является ли время лучшим + time_part = f"{time_str} ({generations_str})" + if best_time is not None and abs(current_time - best_time) < 0.001: + if HIGHLIGHT_COLOR is not None: + time_part = ( + f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{time_part}}}}}" + ) + else: + time_part = f"\\textbf{{{time_part}}}" + + # Проверяем, является ли фитнес лучшим + fitness_part = fitness_str + if best_fitness is not None and abs(current_fitness - best_fitness) < 0.00001: + if HIGHLIGHT_COLOR is not None: + fitness_part = ( + f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{fitness_part}}}}}" + ) + else: + fitness_part = f"\\textbf{{{fitness_part}}}" + + return f"{time_part} {fitness_part}" + + else: + # Нет фитнеса: только "время (поколения)" + time_match = re.match(r"(\d+\.?\d*)\s*\((\d+)\)", value) + if time_match: + current_time = float(time_match.group(1)) + if best_time is not None and abs(current_time - best_time) < 0.001: + if HIGHLIGHT_COLOR is not None: + return f"\\textcolor{{{HIGHLIGHT_COLOR}}}{{\\textbf{{{value}}}}}" + else: + return f"\\textbf{{{value}}}" + + return value def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str: @@ -134,8 +222,9 @@ def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str Returns: LaTeX код таблицы """ - # Находим лучшее время в таблице + # Находим лучшее время и лучший фитнес в таблице best_time = find_best_time(data_rows) + best_fitness = find_best_fitness(data_rows) # Извлекаем заголовки колонок из header header_parts = header.split(",") @@ -162,7 +251,7 @@ def generate_latex_table(n: str, header: str, data_rows: list[list[str]]) -> str # Добавляем значения для каждого Pm for i in range(1, min(6, len(row))): # Максимум 5 колонок Pm - value = format_value(row[i], best_time) + value = format_value(row[i], best_time, best_fitness) latex_code += f" & {value}" # Заполняем недостающие колонки если их меньше 5 @@ -207,9 +296,12 @@ def main(): try: header, data_rows = parse_csv_file(str(csv_file)) best_time = find_best_time(data_rows) + best_fitness = find_best_fitness(data_rows) latex_table = generate_latex_table(n, header, data_rows) tables.append(latex_table) - print(f"✓ Таблица для N={n} готова (лучшее время: {best_time})") + print( + f"✓ Таблица для N={n} готова (лучшее время: {best_time}, лучший фитнес: {best_fitness})" + ) except Exception as e: print(f"✗ Ошибка при обработке {csv_file}: {e}") @@ -221,9 +313,13 @@ def main(): with open("tables.tex", "w", encoding="utf-8") as f: f.write("% Автоматически сгенерированные LaTeX таблицы\n") f.write( - "% Лучший результат по времени выполнения в каждой таблице выделен жирным\n" + "% Лучший результат по времени и по фитнесу выделены жирным отдельно\n" ) f.write("% Убедитесь, что подключен \\usepackage{tabularx}\n") + if HIGHLIGHT_COLOR is not None: + f.write( + "% ВНИМАНИЕ: Убедитесь, что подключен \\usepackage{xcolor} для цветового выделения\n" + ) f.write( "% Используйте \\newcolumntype{Y}{>{\\centering\\arraybackslash}X} перед таблицами\n\n" ) diff --git a/lab2/expirements.py b/lab2/expirements.py index 16c0f51..1852e7d 100644 --- a/lab2/expirements.py +++ b/lab2/expirements.py @@ -19,6 +19,7 @@ BASE_DIR = "experiments" POPULATION_SIZES = [10, 25, 50, 100] PC_VALUES = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8] # вероятности кроссинговера PM_VALUES = [0.001, 0.01, 0.05, 0.1, 0.2] # вероятности мутации +SAVE_AVG_BEST_FITNESS = True # Количество запусков для усреднения результатов NUM_RUNS = 1 @@ -31,7 +32,9 @@ BASE_CONFIG = { "max_generations": 200, "seed": None, # None для случайности, т. к. всё усредняем "minimize": True, - "fitness_avg_threshold": 0.05, # критерий остановки + # "fitness_avg_threshold": 0.05, # критерий остановки + # "max_best_repetitions": 10, + "best_value_threshold": 0.005, # при включенном сохранении графиков на время смотреть бессмысленно # "save_generations": [1, 50, 199], } @@ -39,13 +42,15 @@ BASE_CONFIG = { def run_single_experiment( pop_size: int, pc: float, pm: float -) -> tuple[float, float, float, float]: +) -> tuple[float, float, float, float, float, float]: """ Запускает несколько экспериментов с заданными параметрами и усредняет результаты. - Возвращает (среднее_время_в_мс, стд_отклонение_времени, среднее_поколений, стд_отклонение_поколений). + Возвращает (среднее_время_в_мс, стд_отклонение_времени, среднее_поколений, + стд_отклонение_поколений, среднее_лучшее_значение_фитнеса, стд_отклонение_лучшего_значения_фитнеса). """ times = [] generations = [] + best_fitnesses = [] for run_num in range(NUM_RUNS): config = GARunConfig( @@ -65,14 +70,26 @@ def run_single_experiment( result = genetic_algorithm(config) times.append(result.time_ms) generations.append(result.generations_count) + best_fitnesses.append(result.best_generation.best_fitness) # Вычисляем средние значения и стандартные отклонения avg_time = statistics.mean(times) std_time = statistics.stdev(times) if len(times) > 1 else 0.0 avg_generations = statistics.mean(generations) std_generations = statistics.stdev(generations) if len(generations) > 1 else 0.0 + avg_best_fitness = statistics.mean(best_fitnesses) + std_best_fitness = ( + statistics.stdev(best_fitnesses) if len(best_fitnesses) > 1 else 0.0 + ) - return avg_time, std_time, avg_generations, std_generations + return ( + avg_time, + std_time, + avg_generations, + std_generations, + avg_best_fitness, + std_best_fitness, + ) def run_experiments_for_population(pop_size: int) -> PrettyTable: @@ -92,14 +109,22 @@ def run_experiments_for_population(pop_size: int) -> PrettyTable: row = [f"{pc:.1f}"] for pm in PM_VALUES: print(f" Эксперимент: pop_size={pop_size}, Pc={pc:.1f}, Pm={pm:.3f}") - avg_time, std_time, avg_generations, std_generations = ( - run_single_experiment(pop_size, pc, pm) - ) + ( + avg_time, + std_time, + avg_generations, + std_generations, + avg_best_fitness, + std_best_fitness, + ) = run_single_experiment(pop_size, pc, pm) # Форматируем результат: среднее_время±стд_отклонение (среднее_поколения±стд_отклонение) # cell_value = f"{avg_time:.1f}±{std_time:.1f} ({avg_generations:.1f}±{std_generations:.1f})" cell_value = f"{avg_time:.1f} ({avg_generations:.0f})" + if SAVE_AVG_BEST_FITNESS: + cell_value += f" {avg_best_fitness:.5f}" + if avg_generations == BASE_CONFIG["max_generations"]: cell_value = "—" @@ -118,9 +143,6 @@ def main(): print(f"Значения Pc: {PC_VALUES}") print(f"Значения Pm: {PM_VALUES}") print(f"Количество запусков для усреднения: {NUM_RUNS}") - print( - f"Критерий остановки: среднее значение > {BASE_CONFIG['fitness_avg_threshold']}" - ) print("=" * 60) # Создаем базовую папку diff --git a/lab2/gen.py b/lab2/gen.py index c64d8e0..f6fab19 100644 --- a/lab2/gen.py +++ b/lab2/gen.py @@ -42,6 +42,9 @@ class GARunConfig: fitness_avg_threshold: float | None = ( None # порог среднего значения фитнес функции для остановки ) + best_value_threshold: float | None = ( + None # остановка при достижении значения фитнеса лучше заданного + ) log_every_generation: bool = False # логировать каждое поколение @@ -396,6 +399,15 @@ def genetic_algorithm(config: GARunConfig) -> GARunResult: # if fitness_variance < config.variance_threshold: # stop_algorithm = True + if config.best_value_threshold is not None: + if ( + config.minimize and current.best_fitness < config.best_value_threshold + ) or ( + not config.minimize + and current.best_fitness > config.best_value_threshold + ): + stop_algorithm = True + if config.fitness_avg_threshold is not None: mean_fitness = np.mean(fitnesses) if (config.minimize and mean_fitness < config.fitness_avg_threshold) or (