Загрузка данных


from datetime import datetime
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import quad

import config

METHODS = (
    ("ЛПР", "Метод левых прямоугольников", "left_rectangles"),
    ("ППР", "Метод правых прямоугольников", "right_rectangles"),
    ("СПР", "Метод средних прямоугольников", "midpoint_rectangles"),
    ("ТР", "Метод трапеций", "trapezoids"),
    ("ММК", "Метод Монте-Карло для интеграла", "monte_carlo"),
    ("СИМП", "Метод Симпсона", "simpson"),
)


def f(x):
    """Вычисляем значение функции f(x) = sqrt((4-25*x**2)**3)."""
    x_values = np.asarray(x)
    return np.power(np.maximum(4 - 25 * x_values**2, 0), 1.5)


def true_area():
    """Возвращаем аналитическое (точное численное) значение площади."""
    val, _ = quad(f, config.A, config.B)
    return float(val)


def validate_config():
    """Проверяем корректность входных параметров."""
    if config.B <= config.A:
        raise ValueError("Параметр B должен быть больше A.")
    if not config.N_VALUES:
        raise ValueError("Параметр N_VALUES не должен быть пустым.")
    for n in config.N_VALUES:
        if int(n) <= 0:
            raise ValueError("Все значения N_VALUES должны быть больше нуля.")
    if int(config.PLOT_N) <= 0:
        raise ValueError("Параметр PLOT_N должен быть больше нуля.")


def make_rng(n):
    """Создаём генератор для метода Монте-Карло с устойчивым seed."""
    if getattr(config, 'RANDOM_SEED', None) is None:
        return np.random.default_rng()
    return np.random.default_rng(int(config.RANDOM_SEED) + int(n))


def left_rectangles(n):
    """Вычисляем площадь методом левых прямоугольников."""
    dx = (config.B - config.A) / n
    x_left = config.A + np.arange(n) * dx
    return float(dx * np.sum(f(x_left)))


def right_rectangles(n):
    """Вычисляем площадь методом правых прямоугольников."""
    dx = (config.B - config.A) / n
    x_right = config.A + np.arange(1, n + 1) * dx
    return float(dx * np.sum(f(x_right)))


def midpoint_rectangles(n):
    """Вычисляем площадь методом средних прямоугольников."""
    dx = (config.B - config.A) / n
    x_mid = config.A + (np.arange(n) + 0.5) * dx
    return float(dx * np.sum(f(x_mid)))


def trapezoids(n):
    """Вычисляем площадь методом трапеций."""
    dx = (config.B - config.A) / n
    x_points = np.linspace(config.A, config.B, n + 1)
    y_points = f(x_points)
    return float(dx * (0.5 * y_points[0] + np.sum(y_points[1:-1]) + 0.5 * y_points[-1]))


def monte_carlo(n):
    """Вычисляем площадь методом Монте-Карло для интеграла."""
    rng = make_rng(n)
    x_random = rng.uniform(config.A, config.B, n)
    return float((config.B - config.A) * np.mean(f(x_random)))


def simpson(n):
    """Вычисляем площадь методом Симпсона для n параболических фигур."""
    subintervals = 2 * n
    dx = (config.B - config.A) / subintervals
    x_points = np.linspace(config.A, config.B, subintervals + 1)
    y_points = f(x_points)
    return float(
        dx
        / 3.0
        * (
            y_points[0]
            + y_points[-1]
            + 4.0 * np.sum(y_points[1:-1:2])
            + 2.0 * np.sum(y_points[2:-1:2])
        )
    )


METHOD_FUNCTIONS = {
    "left_rectangles": left_rectangles,
    "right_rectangles": right_rectangles,
    "midpoint_rectangles": midpoint_rectangles,
    "trapezoids": trapezoids,
    "monte_carlo": monte_carlo,
    "simpson": simpson,
}


def calculate_errors(estimate, exact_area):
    absolute_error = abs(estimate - exact_area)
    relative_error = absolute_error / exact_area * 100.0
    return absolute_error, relative_error


def build_results():
    exact_area = true_area()
    results = []
    for short_name, full_name, function_name in METHODS:
        row = {
            "short_name": short_name,
            "full_name": full_name,
            "values": {},
        }
        method = METHOD_FUNCTIONS[function_name]
        for n in config.N_VALUES:
            estimate = method(int(n))
            absolute_error, relative_error = calculate_errors(estimate, exact_area)
            row["values"][int(n)] = {
                "estimate": estimate,
                "absolute_error": absolute_error,
                "relative_error": relative_error,
            }
        results.append(row)
    return results


def format_result_cell(value):
    return (
        f"Ŝ={value['estimate']:.8f}; "
        f"Δ={value['absolute_error']:.8f}; "
        f"δ={value['relative_error']:.4f}%"
    )


def format_results_table(results):
    headers = ["Методы"] + [f"n={int(n)}" for n in config.N_VALUES]
    rows = []
    for row in results:
        rows.append(
            [row["short_name"]]
            + [format_result_cell(row["values"][int(n)]) for n in config.N_VALUES]
        )

    widths = [
        max(len(headers[col]), *(len(row[col]) for row in rows))
        for col in range(len(headers))
    ]
    border = "+-" + "-+-".join("-" * width for width in widths) + "-+"

    lines = [border]
    lines.append(
        "| "
        + " | ".join(headers[col].ljust(widths[col]) for col in range(len(headers)))
        + " |"
    )
    lines.append(border)
    for row in rows:
        lines.append(
            "| "
            + " | ".join(row[col].ljust(widths[col]) for col in range(len(row)))
            + " |"
        )
    lines.append(border)
    return "\n".join(lines)


def find_best_results(results):
    best_by_n = {}
    overall_best = None

    for n in config.N_VALUES:
        n = int(n)
        best_row = min(
            results,
            key=lambda row: row["values"][n]["absolute_error"],
        )
        best_by_n[n] = {
            "short_name": best_row["short_name"],
            "full_name": best_row["full_name"],
            **best_row["values"][n],
        }

    for row in results:
        for n, value in row["values"].items():
            candidate = {
                "n": n,
                "short_name": row["short_name"],
                "full_name": row["full_name"],
                **value,
            }
            if (
                overall_best is None
                or candidate["absolute_error"] < overall_best["absolute_error"]
            ):
                overall_best = candidate

    return best_by_n, overall_best


def format_best_result(value):
    return (
        f"{value['short_name']} ({value['full_name']}): "
        f"Ŝ={value['estimate']:.10f}; "
        f"Δ={value['absolute_error']:.10f}; "
        f"δ={value['relative_error']:.6f}%"
    )


def build_output_basename(output_dir):
    base = getattr(config, 'PLOT_BASENAME', 'plot')
    if getattr(config, 'SAVE_UNIQUE_NAMES', False):
        timestamp = datetime.now().strftime(getattr(config, 'FILENAME_TIMESTAMP_FORMAT', '%Y%m%d_%H%M%S'))
        base = f"{base}_{timestamp}"

    candidate = base
    index = 1
    formats = getattr(config, 'SAVE_FORMATS', ['png'])
    while any((output_dir / f"{candidate}.{ext}").exists() for ext in formats):
        candidate = f"{base}_{index}"
        index += 1

    return candidate


def plot_function(ax):
    samples = getattr(config, 'CURVE_SAMPLES', 400)
    x_line = np.linspace(config.A, config.B, samples)
    y_line = f(x_line)
    ax.plot(
        x_line,
        y_line,
        color=getattr(config, 'FUNCTION_COLOR', 'b'),
        linewidth=2.2,
        label=getattr(config, 'FUNCTION_LABEL', r'$f(x) = \sqrt{(4-25x^2)^3}$'),
        zorder=3,
    )


def setup_method_axis(ax, title):
    ax.set_title(title, fontsize=10)
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_xlim(config.A, config.B)
    ax.set_ylim(0, float(np.max(f(np.linspace(config.A, config.B, 200)))) * 1.12)
    ax.grid(True, alpha=getattr(config, 'GRID_ALPHA', 0.5))


def plot_left_rectangles(ax, n):
    dx = (config.B - config.A) / n
    x_left = config.A + np.arange(n) * dx
    ax.bar(
        x_left,
        f(x_left),
        width=dx,
        align="edge",
        color=getattr(config, 'FIGURE_COLOR', 'orange'),
        edgecolor=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
        alpha=0.35,
        linewidth=1.0,
        label="Левые прямоугольники",
    )


def plot_right_rectangles(ax, n):
    dx = (config.B - config.A) / n
    x_right = config.A + np.arange(1, n + 1) * dx
    ax.bar(
        x_right - dx,
        f(x_right),
        width=dx,
        align="edge",
        color=getattr(config, 'FIGURE_COLOR', 'green'),
        edgecolor=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
        alpha=0.35,
        linewidth=1.0,
        label="Правые прямоугольники",
    )


def plot_midpoint_rectangles(ax, n):
    dx = (config.B - config.A) / n
    x_left = config.A + np.arange(n) * dx
    x_mid = x_left + 0.5 * dx
    ax.bar(
        x_left,
        f(x_mid),
        width=dx,
        align="edge",
        color=getattr(config, 'FIGURE_COLOR', 'red'),
        edgecolor=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
        alpha=0.35,
        linewidth=1.0,
        label="Средние прямоугольники",
    )
    ax.scatter(x_mid, f(x_mid), s=18, color=getattr(config, 'FIGURE_EDGE_COLOR', 'black'), zorder=4)


def plot_trapezoids(ax, n):
    x_points = np.linspace(config.A, config.B, n + 1)
    y_points = f(x_points)
    for i in range(n):
        polygon_x = [x_points[i], x_points[i], x_points[i + 1], x_points[i + 1]]
        polygon_y = [0, y_points[i], y_points[i + 1], 0]
        ax.fill(
            polygon_x,
            polygon_y,
            color=getattr(config, 'FIGURE_COLOR', 'purple'),
            edgecolor=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
            alpha=0.32,
            linewidth=1.0,
        )
    ax.plot(x_points, y_points, color=getattr(config, 'FIGURE_EDGE_COLOR', 'black'), linewidth=1.2, label="Трапеции")


def plot_monte_carlo(ax, n):
    rng = make_rng(n)
    x_random = np.sort(rng.uniform(config.A, config.B, n))
    y_random = f(x_random)
    width = (config.B - config.A) / n
    ax.bar(
        x_random,
        y_random,
        width=width * 0.85,
        align="center",
        color=getattr(config, 'MC_COLOR', 'green'),
        edgecolor=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
        alpha=0.32,
        linewidth=0.9,
        label="Случайные прямоугольники",
    )
    ax.scatter(x_random, y_random, s=20, color=getattr(config, 'MC_COLOR', 'green'), zorder=4)


def plot_simpson(ax, n):
    subintervals = 2 * n
    x_points = np.linspace(config.A, config.B, subintervals + 1)
    y_points = f(x_points)
    for figure_index in range(n):
        i = 2 * figure_index
        local_x = x_points[i : i + 3]
        local_y = y_points[i : i + 3]
        coefficients = np.polyfit(local_x, local_y, deg=2)
        x_dense = np.linspace(local_x[0], local_x[-1], 80)
        y_dense = np.polyval(coefficients, x_dense)
        ax.fill_between(
            x_dense,
            0,
            y_dense,
            color=getattr(config, 'SIMPSON_COLOR', 'cyan'),
            alpha=0.28,
            edgecolor=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
            linewidth=0.8,
        )
        ax.plot(x_dense, y_dense, color=getattr(config, 'SIMPSON_COLOR', 'cyan'), linewidth=1.2)
        ax.vlines(
            [local_x[0], local_x[-1]],
            0,
            [local_y[0], local_y[-1]],
            color=getattr(config, 'FIGURE_EDGE_COLOR', 'black'),
            alpha=0.55,
            linewidth=0.8,
        )
    ax.scatter(x_points, y_points, s=16, color=getattr(config, 'FIGURE_EDGE_COLOR', 'black'), zorder=4, label="Узлы")


PLOTTERS = {
    "left_rectangles": plot_left_rectangles,
    "right_rectangles": plot_right_rectangles,
    "midpoint_rectangles": plot_midpoint_rectangles,
    "trapezoids": plot_trapezoids,
    "monte_carlo": plot_monte_carlo,
    "simpson": plot_simpson,
}


def plot_results():
    output_dir = Path(__file__).resolve().parent / getattr(config, 'PLOT_DIR', 'plots')
    output_dir.mkdir(parents=True, exist_ok=True)

    n = int(config.PLOT_N)
    fig, axes = plt.subplots(3, 2, figsize=getattr(config, 'FIGURE_SIZE', (12, 10)))
    axes_flat = axes.ravel()
    exact_area = true_area()

    fig.suptitle(
        f"{getattr(config, 'PLOT_TITLE', 'Численное интегрирование')}\n"
        f"{getattr(config, 'FUNCTION_LABEL', 'f(x)')}, Sист={exact_area:.8f}, n={n}",
        fontsize=13,
    )

    for ax, (short_name, full_name, function_name) in zip(axes_flat, METHODS):
        PLOTTERS[function_name](ax, n)
        plot_function(ax)
        estimate = METHOD_FUNCTIONS[function_name](n)
        absolute_error, relative_error = calculate_errors(estimate, exact_area)
        setup_method_axis(
            ax,
            f"{short_name}: {full_name}\n"
            f"Ŝ={estimate:.6f}; Δ={absolute_error:.6f}; δ={relative_error:.3f}%",
        )
        ax.legend(loc="best", fontsize=8)

    plt.tight_layout(rect=(0, 0, 1, 0.94))

    base_name = build_output_basename(output_dir)
    saved_paths = []
    formats = getattr(config, 'SAVE_FORMATS', ['png'])
    for extension in formats:
        file_path = output_dir / f"{base_name}.{extension}"
        fig.savefig(file_path, dpi=getattr(config, 'PLOT_DPI', 100), bbox_inches="tight")
        saved_paths.append(file_path)

    backend = plt.get_backend().lower()
    if getattr(config, 'SHOW_PLOT', True) and "agg" not in backend:
        plt.show()
    else:
        plt.close(fig)

    return saved_paths


def print_report(results, saved_paths):
    exact_value = true_area()

    print("=" * 76)
    print("ЛАБОРАТОРНАЯ РАБОТА №4")
    print("Вычисление площади функции различными методами")
    print("=" * 76)
    print(f"f(x) = sqrt((4 - 25x^2)^3)")
    print(f"Отрезок интегрирования: [{config.A}, {config.B}]")
    print(f"Sист = {exact_value:.10f}")
    print("-" * 76)
    print(format_results_table(results))
    print("-" * 76)
    best_by_n, overall_best = find_best_results(results)
    print("ЛУЧШИЙ МЕТОД ПО МИНИМАЛЬНОЙ АБСОЛЮТНОЙ ПОГРЕШНОСТИ")
    for n in config.N_VALUES:
        n = int(n)
        print(f"n={n}: {format_best_result(best_by_n[n])}")
    print("Лучший результат по всей таблице:")
    print(f"n={overall_best['n']}: {format_best_result(overall_best)}")
    print("=" * 76)
    print("Файлы графика:")
    for path in saved_paths:
        print(f"- {path}")


def main():
    try:
        validate_config()
    except ValueError as exc:
        raise SystemExit(f"Ошибка в параметрах config.py: {exc}") from exc

    results = build_results()
    saved_paths = plot_results()
    print_report(results, saved_paths)


if __name__ == "__main__":
    main()