{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "221ed277",
   "metadata": {},
   "source": [
    "# Анализ максимальных напряжений в отводе трубопровода"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea5e8434",
   "metadata": {},
   "source": [
    "В данном ноутбуке представлен код для создания модели нейронной сети с использованием PyTorch. Решается задача предсказания максимальных напряжений, возникающих в отводе куста газовых скважин, при известных параметрах системы. \n",
    "\n",
    "Код подготовлен компанией [АО «ИТЦ «ДЖЭТ»](https://get-sim.ru) для демонстрации интеграции возможностей импорта моделей машинного обучения на [платформу REPEAT VISION](https://app.repeat.vision) для выполнения инженерных расчётов. Для более полного ознакомления с возможностями платформы рекомендуется ознакомиться с документацией [сайте](https://app.repeat.vision/docs/)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec996fb2",
   "metadata": {},
   "source": [
    "### Описание задачи"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8edb4f4",
   "metadata": {},
   "source": [
    "\n",
    "Инженерная система газовых скважин представляет собой сеть трубопроводов и отводов, по которым транспортируется добываемый газ с высокими скоростями и давлением. Со временем на внутренней поверхности труб под действием потока абразивных частиц начинается эрозия металла, что ведёт к уменьшению толщины стенки в наиболее нагруженных участках.\n",
    "\n",
    "Целью является разработка и обучение математической модели, способной по совокупности параметров предсказывать величину внутренних напряжений зоны, подверженной эрозионному износу. В результате использования модели оператор получит оценки напряжения, что позволит своевременно проводить ремонт или замену участков труб, минимизируя простой оборудования и риск аварий."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29cb907a",
   "metadata": {},
   "source": [
    "### Используемые библиотеки"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36d95323",
   "metadata": {},
   "source": [
    "В данном примере используется множество библиотек для анализа данных и машинного обучения. Их можно разделить на несколько категорий:\n",
    "\n",
    "- Библиотеки для работы с данными:\n",
    "    - **NumPy** - фундаментальная библиотека для научных вычислений, работы с многомерными массивами и матрицами.\n",
    "    - **Pandas** - библиотека для обработки и анализа табличных данных.\n",
    "- Библиотеки для визуализации:\n",
    "    - **Matplotlib** и **Seaborn** - библиотеки для создания статистических графиков.\n",
    "    - **Plotly** - библиотека для создания интерактивных визуализаций.\n",
    "- Библиотеки для машинного обучения:\n",
    "    - **PyTorch** - фреймворк для создания и обучения нейронных сетей. Включает модули с основными структурными компонентами (**torch.nn**), функциями (**torch.nn.functional**), инструментами оптимизации (**torch.optim**) и другие инструменты (**torch.utils.data**).\n",
    "- Библиотеки для оценки качества модели:\n",
    "    - **SciKit-Learn** - библиотека для классического машинного обучения.\n",
    "\n",
    "> Для работы с GPU необходимо установить версию PyTorch, совместимую с вашей версией CUDA. Рекомендуется посетить официальный сайт PyTorch для получения инструкций по установке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7a44b1da",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "import plotly.graph_objects as go\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.metrics import (mean_squared_error, \n",
    "                             mean_absolute_error,\n",
    "                             mean_absolute_percentage_error,\n",
    "                             r2_score)\n",
    "\n",
    "from tqdm.notebook import tqdm\n",
    "from typing import Tuple, List, Dict, Union"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41854bb2",
   "metadata": {},
   "source": [
    "Установка фиксированного seed для воспроизводимости результатов"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c41e6277",
   "metadata": {},
   "outputs": [],
   "source": [
    "SEED = 42\n",
    "torch.manual_seed(SEED)\n",
    "np.random.seed(SEED)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fab493d0",
   "metadata": {},
   "source": [
    "Определение устройства для вычислений (GPU или CPU)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "836d2b70",
   "metadata": {},
   "outputs": [],
   "source": [
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "print(device)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "648c8cd6",
   "metadata": {},
   "source": [
    "### Построение 3D модели отвода"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b969668",
   "metadata": {},
   "source": [
    "График ниже представляет собой 3D визуализацию распределения напряжений в отводе трубопровода. Он показывает, как напряжения распределяются по различным координатам в конструкции. Тёплые оттенки указывают на высокие значения напряжений, а более холодные – на низкие."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a2341054",
   "metadata": {},
   "outputs": [],
   "source": [
    "def branch_plot(df: pd.DataFrame) -> go.Figure:\n",
    "    \"\"\"\n",
    "    Создает 3D визуализацию распределения напряжений.\n",
    "    \n",
    "    Параметры:\n",
    "        df: pd.DataFrame\n",
    "            DataFrame с данными о координатах и напряжениях\n",
    "        \n",
    "    Возвращает:\n",
    "        go.Figure: Объект графика Plotly\n",
    "    \"\"\"\n",
    "    marker_config = dict(\n",
    "        size=3,\n",
    "        color=df['Stress'],\n",
    "        colorscale='Jet',\n",
    "        opacity=0.8,\n",
    "        colorbar=dict(title=\"Напряжение (МПа)\")\n",
    "    )\n",
    "    \n",
    "    hover_template = (\n",
    "        '<b>Узел</b>: %{customdata}<br>'\n",
    "        '<b>X</b>: %{x:.2f} мм<br>'\n",
    "        '<b>Y</b>: %{y:.2f} мм<br>'\n",
    "        '<b>Z</b>: %{z:.2f} мм<br>'\n",
    "        '<b>Напряжение</b>: %{marker.color:.2f} МПа'\n",
    "    )\n",
    "    \n",
    "    fig = go.Figure(\n",
    "        data=[\n",
    "            go.Scatter3d(\n",
    "                x=df['X'],\n",
    "                y=df['Y'],\n",
    "                z=df['Z'],\n",
    "                mode='markers',\n",
    "                marker=marker_config,\n",
    "                hovertemplate=hover_template,\n",
    "                customdata=df['Node']\n",
    "            )\n",
    "        ]\n",
    "    )\n",
    "    \n",
    "    scene_config = dict(\n",
    "        xaxis_title='X (мм)',\n",
    "        yaxis_title='Y (мм)',\n",
    "        zaxis_title='Z (мм)',\n",
    "        aspectmode='data',\n",
    "        camera=dict(\n",
    "            eye=dict(x=0, y=0, z=2.5),\n",
    "            center=dict(x=0, y=0, z=0),\n",
    "            up=dict(x=0, y=1, z=0)\n",
    "        )\n",
    "    )\n",
    "    \n",
    "    margin_config = dict(l=0, r=0, b=0, t=30)\n",
    "    \n",
    "    fig.update_layout(\n",
    "        title='Распределение напряжений',\n",
    "        scene=scene_config,\n",
    "        margin=margin_config\n",
    "    )\n",
    "\n",
    "    return fig"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5924a833",
   "metadata": {},
   "outputs": [],
   "source": [
    "file_path = 'stress_branch.csv'\n",
    "df = pd.read_csv(file_path)\n",
    "fig = branch_plot(df)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f8ccd3f",
   "metadata": {},
   "source": [
    "### Данные для обучения"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eba476af",
   "metadata": {},
   "source": [
    "Набор данных представляет собой таблицу с информацией о параметрах отвода трубопровода и максимальном напряжении.\n",
    "\n",
    "Рассмотрим основные характеристики:\n",
    "- Входные параметры:\n",
    "    - Давление (pressure), меняется в диапазоне: 6.0 - 8.5 МПа.\n",
    "    - Температура газа (gas_temperature), меняется в диапазоне: 7.0 - 20.0 K.\n",
    "    - Температура окружающей среды (ambient_temperature), меняется в диапазоне: -55.0 - 40.0 K.\n",
    "    - Минимальная толщина стенки (wall_thickness_min), меняется в диапазоне: 4.90 - 6.45 мм.\n",
    "- Целевая переменная:\n",
    "    - Максимальное напряжение по Мизесу в конструкции (stress_max), меняется в диапазоне: 45.31 - 140.40 МПа."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "595b949d",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('branch_data.csv')\n",
    "df.describe()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23ded45a",
   "metadata": {},
   "outputs": [],
   "source": [
    "features = ['pressure', 'gas_temp', 'ambient_temp', 'wall_thickness']\n",
    "target = 'stress_max'\n",
    "\n",
    "df[features + [target]].head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f4f9467",
   "metadata": {},
   "source": [
    "#### Класс StressDataset\n",
    "\n",
    "Класс `StressDataset` представляет исходный набор данных в формат, удобный для работы с PyTorch. Класс наследуется от базового класса `torch.utils.data.Dataset`.\n",
    "\n",
    "Внутри класса входные данные преобразуются в тензоры PyTorch, целевые значения преобразуются в тензор с формой [1, 1], все данные перемещаются на указанное устройство (CPU/GPU) при вызове их из контейнера данных, каждый признак обрабатывается как отдельный тензор (это будет необходимо для дальнейшего импорта модели в REPEAT)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35db0eb7",
   "metadata": {},
   "outputs": [],
   "source": [
    "class StressDataset(Dataset):\n",
    "    \"\"\"\n",
    "    Класс датасета для обучения модели.\n",
    "    \"\"\"\n",
    "    def __init__(\n",
    "        self,\n",
    "        X: np.ndarray,\n",
    "        y: np.ndarray,\n",
    "        device: torch.device = torch.device('cpu')\n",
    "    ) -> None:\n",
    "        \"\"\"\n",
    "        Инициализация набора данных.\n",
    "\n",
    "        Параметры:\n",
    "            X: np.ndarray\n",
    "                Массив входных признаков\n",
    "            y: np.ndarray\n",
    "                Массив целевых значений\n",
    "            device: torch.device\n",
    "                Устройство для вычислений\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.device = device\n",
    "        self.X = X\n",
    "        self.y = torch.FloatTensor(y, device=self.device).reshape(-1, 1)\n",
    "    \n",
    "    def __len__(self) -> int:\n",
    "        \"\"\"\n",
    "        Методы получения количества элементов.\n",
    "        \n",
    "        Возвращает:\n",
    "            int: Количество элементов\n",
    "        \"\"\"\n",
    "        return len(self.X)\n",
    "    \n",
    "    def __getitem__(self, idx: int) -> Tuple[Tuple[torch.Tensor, ...], torch.Tensor]:\n",
    "        \"\"\"\n",
    "        Возвращает пару (признаки, целевое значение) для заданного индекса.\n",
    "        \n",
    "        Параметры:\n",
    "            idx: int\n",
    "                Индекс образца\n",
    "            \n",
    "        Возвращает:\n",
    "            Tuple[Tuple[torch.Tensor, ...], torch.Tensor]: \n",
    "                Кортеж из кортежа признаков и целевого значения\n",
    "        \"\"\"\n",
    "        return (\n",
    "            tuple(torch.Tensor([feature], device=self.device) \n",
    "                  for feature in self.X[idx]),\n",
    "            self.y[idx]\n",
    "        )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a793d61",
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df[features].values\n",
    "y = df[target].values\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    X, y,\n",
    "    test_size=0.2,\n",
    "    random_state=42\n",
    ")\n",
    "\n",
    "train_dataset = StressDataset(X_train, y_train, device)\n",
    "test_dataset = StressDataset(X_test, y_test, device)\n",
    "\n",
    "batch_size = 64\n",
    "train_loader = DataLoader(\n",
    "    train_dataset, \n",
    "    batch_size=batch_size, \n",
    "    shuffle=True\n",
    ")\n",
    "test_loader = DataLoader(\n",
    "    test_dataset, \n",
    "    batch_size=batch_size, \n",
    "    shuffle=False\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a64ccaa9",
   "metadata": {},
   "source": [
    "### Нейронная сеть"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b44062eb",
   "metadata": {},
   "source": [
    "Класс `StressNet` нейронной сети для предсказания максимального напряжения. Класс наследуется от базового класс `nn.Module`. Нейронная сеть представляет собой трехслойный MLP, с одинаковым размером скрытых скрытых слоев, встроенной нормализация входных данных, `LayerNorm` после каждого полносвязного слоя и `Dropout` для регуляризации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f549d6d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "class StressNet(nn.Module):\n",
    "    \"\"\"\n",
    "    Нейронная сеть.\n",
    "    \"\"\"\n",
    "    def __init__(\n",
    "        self,\n",
    "        input_size: int = 4,\n",
    "        hidden_size: int = 32,\n",
    "        mean: torch.Tensor = torch.zeros(4),\n",
    "        std: torch.Tensor = torch.ones(4),\n",
    "        dropout_rate: float = 0.2\n",
    "    ) -> None:\n",
    "        \"\"\"\n",
    "        Инициализация модели.\n",
    "        \n",
    "        Параметры:\n",
    "            input_size: int\n",
    "                Размер входного слоя\n",
    "            hidden_size: int\n",
    "                Размер скрытых слоев\n",
    "            mean: torch.Tensor\n",
    "                Средние значения для нормализации\n",
    "            std: torch.Tensor\n",
    "                Стандартные отклонения для нормализации\n",
    "            dropout_rate: float\n",
    "                Вероятность отключения нейронов\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        \n",
    "        self.fc1 = nn.Linear(input_size, hidden_size)\n",
    "        self.norm1 = nn.LayerNorm(hidden_size)\n",
    "        self.fc2 = nn.Linear(hidden_size, hidden_size)\n",
    "        self.norm2 = nn.LayerNorm(hidden_size)\n",
    "        self.fc3 = nn.Linear(hidden_size, hidden_size)\n",
    "        self.norm3 = nn.LayerNorm(hidden_size)\n",
    "        self.out = nn.Linear(hidden_size, 1)\n",
    "\n",
    "        self.act = nn.ReLU()\n",
    "        self.dropout = nn.Dropout(dropout_rate)\n",
    "\n",
    "        self.mean = mean\n",
    "        self.std = std\n",
    "\n",
    "        self.apply(self._init_weights)\n",
    "\n",
    "    def _init_weights(self, module: nn.Module) -> None:\n",
    "        \"\"\"\n",
    "        Инициализация весов слоев.\n",
    "        \n",
    "        Параметры:\n",
    "            module: nn.Module\n",
    "                Модуль для инициализации весов\n",
    "        \"\"\"\n",
    "        if isinstance(module, nn.Linear):\n",
    "            nn.init.kaiming_uniform_(\n",
    "                module.weight,\n",
    "                nonlinearity='linear'\n",
    "            )\n",
    "            nn.init.zeros_(module.bias)\n",
    "\n",
    "    def forward(\n",
    "        self,\n",
    "        pressure: torch.Tensor,\n",
    "        gas_temp: torch.Tensor,\n",
    "        ambient_temp: torch.Tensor,\n",
    "        wall_thickness: torch.Tensor\n",
    "    ) -> torch.Tensor:\n",
    "        \"\"\"\n",
    "        Прямой проход через сеть.\n",
    "        \n",
    "        Параметры:\n",
    "            pressure: torch.Tensor\n",
    "                Давление\n",
    "            gas_temp: torch.Tensor\n",
    "                Температура газа\n",
    "            ambient_temp: torch.Tensor\n",
    "                Температура окружения\n",
    "            wall_thickness: torch.Tensor\n",
    "                Толщина стенки\n",
    "            \n",
    "        Возвращает:\n",
    "            torch.Tensor: Предсказанное значение максимального напряжения\n",
    "        \"\"\"\n",
    "        batch_size = pressure.shape[0]\n",
    "        \n",
    "        x = torch.cat([\n",
    "            pressure.view(batch_size, -1),\n",
    "            gas_temp.view(batch_size, -1),\n",
    "            ambient_temp.view(batch_size, -1),\n",
    "            wall_thickness.view(batch_size, -1)\n",
    "        ], dim=1)\n",
    "\n",
    "        x = (x - self.mean) / self.std\n",
    "\n",
    "        x = self.fc1(x)\n",
    "        x = self.norm1(x)\n",
    "        x = self.act(x)\n",
    "        x = self.dropout(x)\n",
    "\n",
    "        x = self.fc2(x)\n",
    "        x = self.norm2(x)\n",
    "        x = self.act(x)\n",
    "        x = self.dropout(x)\n",
    "\n",
    "        x = self.fc3(x)\n",
    "        x = self.norm3(x)\n",
    "        x = self.act(x)\n",
    "        x = self.dropout(x)\n",
    "\n",
    "        return self.out(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "341b7505",
   "metadata": {},
   "source": [
    "### Функция обучения модели"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17923450",
   "metadata": {},
   "source": [
    "Функция `train_model` предназначена для обучения нейронной сети с использованием метода градиентного спуска. Вот основные компоненты и этапы работы функции:\n",
    "- **Подготовка**:\n",
    "    - Перенос модели на указанное устройство.\n",
    "    - Инициализация списков для отслеживания потерь.\n",
    "    - Настройка ранней остановки (early stopping).\n",
    "- **Цикл обучения**:\n",
    "    - Обучение на тренировочных данных.\n",
    "    - Валидация на проверочных данных.\n",
    "    - Отслеживание лучшей модели.\n",
    "    - Проверка условий ранней остановки.\n",
    "- **Мониторинг**:\n",
    "    - Отслеживание значения функции потерь.\n",
    "    - Сохранение лучшей модели.\n",
    "    - Вывод прогресса обучения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a77267f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_model(\n",
    "    model: nn.Module,\n",
    "    train_loader: DataLoader,\n",
    "    val_loader: DataLoader,\n",
    "    criterion: nn.Module,\n",
    "    optimizer: optim.Optimizer,\n",
    "    epochs: int,\n",
    "    device: torch.device = torch.device('cpu'),\n",
    "    patience: int = 20\n",
    ") -> Tuple[nn.Module, List[float], List[float]]:\n",
    "    \"\"\"\n",
    "    Обучает модель нейронной сети.\n",
    "    \n",
    "    Параметры:\n",
    "        model: nn.Module\n",
    "            Модель нейронной сети для обучения\n",
    "        train_loader: DataLoader\n",
    "            Загрузчик данных для обучающей выборки\n",
    "        val_loader: DataLoader\n",
    "            Загрузчик данных для валидационной выборки\n",
    "        criterion: nn.Module\n",
    "            Функция потерь\n",
    "        optimizer: optim.Optimizer\n",
    "            Оптимизатор для обновления весов\n",
    "        epochs: int\n",
    "            Количество эпох обучения\n",
    "        device: torch.device\n",
    "            Устройство для вычислений (CPU/GPU)\n",
    "        patience: int\n",
    "            Количество проведенных эпох без улучшения, достаточное для ранней остановки обучения\n",
    "        \n",
    "    Возвращает:\n",
    "        Tuple[nn.Module, List[float], List[float]]: Обученная модель, \n",
    "            список потерь на обучающей выборке, список потерь на валидационной выборке\n",
    "    \"\"\"\n",
    "    model.to(device)\n",
    "    \n",
    "    train_losses = []\n",
    "    val_losses = []\n",
    "    \n",
    "    best_val_loss = float('inf')\n",
    "    no_improvement = 0\n",
    "    best_model_state = None\n",
    "    \n",
    "    for epoch in tqdm(range(epochs), desc='Training'):\n",
    "        model.train()\n",
    "        train_loss = 0.0\n",
    "        \n",
    "        for inputs, targets in train_loader:\n",
    "            optimizer.zero_grad()\n",
    "            \n",
    "            outputs = model(*inputs)\n",
    "            loss = criterion(outputs, targets)\n",
    "            \n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            \n",
    "            train_loss += loss.item() * targets.size(0)\n",
    "            \n",
    "        train_loss = train_loss / len(train_loader.dataset)\n",
    "        train_losses.append(train_loss)\n",
    "        \n",
    "        model.eval()\n",
    "        val_loss = 0.0\n",
    "        \n",
    "        with torch.no_grad():\n",
    "            for inputs, targets in val_loader:\n",
    "                outputs = model(*inputs)\n",
    "                loss = criterion(outputs, targets)\n",
    "                \n",
    "                val_loss += loss.item() * targets.size(0)\n",
    "                \n",
    "        val_loss = val_loss / len(val_loader.dataset)\n",
    "        val_losses.append(val_loss)\n",
    "        \n",
    "        if epoch % 10 == 0:\n",
    "            print(\n",
    "                f'Epoch {epoch+1}/{epochs}, '\n",
    "                f'Train Loss: {train_loss:.6f}, '\n",
    "                f'Val Loss: {val_loss:.6f}'\n",
    "            )\n",
    "        \n",
    "        if val_loss < best_val_loss:\n",
    "            best_val_loss = val_loss\n",
    "            no_improvement = 0\n",
    "            best_model_state = model.state_dict().copy()\n",
    "        else:\n",
    "            no_improvement += 1\n",
    "            if no_improvement >= patience:\n",
    "                print(f'Early stopping at epoch {epoch+1}')\n",
    "                model.load_state_dict(best_model_state)\n",
    "                break\n",
    "    \n",
    "    return model, train_losses, val_losses"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23b08754",
   "metadata": {},
   "source": [
    "### Функции оценка модели"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec617b6c",
   "metadata": {},
   "source": [
    "Функция `evaluate_model` предназначена для оценки производительности обученной нейронной сети на тестовом наборе данных. Она выполняет следующие действия:\n",
    "\n",
    "1. **Подготовка модели**:\n",
    "   - Переводит модель в режим оценки (`model.eval()`).\n",
    "   - Отключает вычисление градиентов для оптимизации памяти.\n",
    "\n",
    "2. **Обработка данных**:\n",
    "   - Проходит по всем батчам в DataLoader.\n",
    "   - Получает предсказания модели.\n",
    "   - Собирает реальные значения и предсказания в списки.\n",
    "\n",
    "3. **Расчет метрик**:\n",
    "   - MSE (Mean Squared Error) - средняя квадратичная ошибка.\n",
    "   - RMSE (Root Mean Squared Error) - корень из средней квадратичной ошибки.\n",
    "   - MAPE (Mean Absolute Percentage Error) - средняя абсолютная процентная ошибка.\n",
    "   - MAE (Mean Absolute Error) - средняя абсолютная ошибка.\n",
    "   - R² (R-squared) - коэффициент детерминации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a32ac6e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def evaluate_model(\n",
    "    model: nn.Module,\n",
    "    data_loader: DataLoader,\n",
    "    criterion: nn.Module,\n",
    "    device: torch.device = torch.device('cpu')\n",
    ") -> Dict[str, Union[float, np.ndarray]]:\n",
    "    \"\"\"\n",
    "    Оценивает производительность модели на заданном наборе данных.\n",
    "    \n",
    "    Параметры:\n",
    "        model: nn.Module\n",
    "            Модель PyTorch для оценки\n",
    "        data_loader: DataLoader\n",
    "            DataLoader с данными для оценки\n",
    "        criterion: nn.Module\n",
    "            Функция потерь\n",
    "        device: torch.device\n",
    "            Устройство для вычислений\n",
    "        \n",
    "    Возвращает:\n",
    "        Dict[str, Union[float, np.ndarray]]: Словарь с метриками оценки\n",
    "        и результатами предсказаний\n",
    "    \"\"\"\n",
    "    model.to(device)\n",
    "    model.eval()\n",
    "    \n",
    "    all_targets = []\n",
    "    all_predictions = []\n",
    "    total_loss = 0.0\n",
    "    \n",
    "    with torch.no_grad():\n",
    "        for inputs, targets in data_loader:\n",
    "            outputs = model(*inputs)\n",
    "            loss = criterion(outputs, targets)\n",
    "            total_loss += loss.item() * targets.size(0)\n",
    "            \n",
    "            all_targets.extend(targets.cpu().numpy())\n",
    "            all_predictions.extend(outputs.cpu().numpy())\n",
    "    \n",
    "    all_targets = np.array(all_targets).flatten()\n",
    "    all_predictions = np.array(all_predictions).flatten()\n",
    "    \n",
    "    mse = mean_squared_error(all_targets, all_predictions)\n",
    "    rmse = np.sqrt(mse)\n",
    "    mape = mean_absolute_percentage_error(all_targets, all_predictions)\n",
    "    mae = mean_absolute_error(all_targets, all_predictions)\n",
    "    r2 = r2_score(all_targets, all_predictions)\n",
    "    \n",
    "    avg_loss = total_loss / len(data_loader.dataset)\n",
    "    \n",
    "    return {\n",
    "        'loss': avg_loss,\n",
    "        'mse': mse,\n",
    "        'rmse': rmse,\n",
    "        'mape': mape,\n",
    "        'mae': mae,\n",
    "        'r2': r2,\n",
    "        'targets': all_targets,\n",
    "        'predictions': all_predictions\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07ef1bd8",
   "metadata": {},
   "source": [
    "Функция `visualize_results` предназначена для визуализации динамики изменения функции потерь на обучающей и валидационной выборках  и построения гистограммы ошибок."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4e526af",
   "metadata": {},
   "outputs": [],
   "source": [
    "def visualize_results(\n",
    "    train_losses: List[float],\n",
    "    val_losses: List[float],\n",
    "    test_results: Dict[str, np.ndarray]\n",
    ") -> None:\n",
    "    \"\"\"\n",
    "    Визуализирует результаты обучения модели.\n",
    "    \n",
    "    Параметры:\n",
    "        train_losses: List[float]\n",
    "            Список значений функции потерь на обучающей выборке\n",
    "        val_losses: List[float]\n",
    "            Список значений функции потерь на валидационной выборке\n",
    "        test_results: Dict[str, np.ndarray]\n",
    "            Словарь с результатами тестирования модели\n",
    "    \"\"\"\n",
    "    _, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n",
    "    \n",
    "    ax1.plot(train_losses, label='Train Loss')\n",
    "    ax1.plot(val_losses, label='Val Loss')\n",
    "    ax1.set_xlabel('Epoch')\n",
    "    ax1.set_ylabel('Loss')\n",
    "    ax1.legend()\n",
    "    ax1.grid(True)\n",
    "    \n",
    "    errors: np.ndarray = test_results['predictions'] - test_results['targets']\n",
    "    sns.histplot(errors, bins=30)\n",
    "    ax2.set_xlabel('Error')\n",
    "    ax2.set_ylabel('Frequency')\n",
    "    ax2.grid(True)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16a18161",
   "metadata": {},
   "source": [
    "Функция `export_to_onnx` предназначена для экспорта обученной PyTorch модели в формат ONNX. Функция учитывает необходимости перевода в режим `double` и учитывает имена входных и выходных параметров."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a049e652",
   "metadata": {},
   "source": [
    "### Создание, обучение и оценка нейронной сети"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb411c1b",
   "metadata": {},
   "source": [
    "Создадим нейронную сеть для предсказания максимального напряжения. Используется трехслойная полносвязная архитектура.\n",
    "Для стабильного обучения проведем нормализацию входных данных - для этого статистические параметры данных передаются в модель."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c90d8d79",
   "metadata": {},
   "outputs": [],
   "source": [
    "hidden_size = 512\n",
    "dropout_rate = 0.2\n",
    "\n",
    "stats = pd.DataFrame(df[features])\n",
    "mean = torch.tensor(stats.mean().values, dtype=torch.float32)\n",
    "std = torch.tensor(stats.std().values,  dtype=torch.float32)\n",
    "\n",
    "model = StressNet(\n",
    "    input_size=len(features),\n",
    "    mean=mean,\n",
    "    std=std,\n",
    "    hidden_size=hidden_size,\n",
    "    dropout_rate=dropout_rate \n",
    ")\n",
    "\n",
    "criterion = nn.MSELoss()\n",
    "optimizer = optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ca0c66e",
   "metadata": {},
   "outputs": [],
   "source": [
    "model, train_losses, val_losses = train_model(\n",
    "    model=model,\n",
    "    train_loader=train_loader,\n",
    "    val_loader=test_loader,\n",
    "    criterion=criterion,\n",
    "    optimizer=optimizer,\n",
    "    epochs=500,\n",
    "    device=device,\n",
    "    patience=10\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cae7b1b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "test_results = evaluate_model(model, test_loader, criterion, device)\n",
    "\n",
    "print(\"Test metrics:\")\n",
    "print(f\"MSE: {test_results['loss']:.3f}\")\n",
    "print(f\"RMSE: {test_results['rmse']:.3f}\")\n",
    "print(f\"MAPE: {test_results['mape']*100:.3f}%\")\n",
    "print(f\"MAE: {test_results['mae']:.3f}\")\n",
    "print(f\"R^2: {test_results['r2']:.3f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "517c4690",
   "metadata": {},
   "outputs": [],
   "source": [
    "visualize_results(train_losses, val_losses, test_results)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd444109",
   "metadata": {},
   "source": [
    "### Экспорт модели в ONNX"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9208c5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def export_to_onnx(\n",
    "    model: nn.Module,\n",
    "    features: List[str],\n",
    "    target: str,\n",
    "    output_path: str = \"stress_model.onnx\"\n",
    ") -> None:\n",
    "    \"\"\"\n",
    "    Экспортирует PyTorch модель в формат ONNX.\n",
    "    \n",
    "    Args:\n",
    "        model: nn.Module\n",
    "            PyTorch модель для экспорта\n",
    "        features: List[str]\n",
    "            Список имен входных признаков\n",
    "        target: str\n",
    "            Имя целевой переменной\n",
    "        output_path: str\n",
    "            Путь для сохранения ONNX модели\n",
    "    \"\"\"\n",
    "    model.eval()\n",
    "    model.double()\n",
    "    \n",
    "    dummy_inputs: Dict[str, torch.Tensor] = {\n",
    "        feature: torch.zeros(1, 1, dtype=torch.float64)\n",
    "        for feature in features\n",
    "    }\n",
    "    \n",
    "    torch.onnx.export(\n",
    "        model,\n",
    "        dummy_inputs,\n",
    "        output_path,\n",
    "        input_names=features,\n",
    "        output_names=[target],\n",
    "        opset_version=12,\n",
    "        dynamo = False\n",
    "    )\n",
    "    \n",
    "    print(f\"Model exported to {output_path}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "809a0059",
   "metadata": {},
   "outputs": [],
   "source": [
    "export_to_onnx(model, features, target)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8bcee55",
   "metadata": {},
   "outputs": [],
   "source": [
    "import netron\n",
    "from IPython.display import IFrame\n",
    "\n",
    "onnx_model_path = \"stress_model.onnx\"\n",
    "\n",
    "url = netron.start(onnx_model_path, browse=False)\n",
    "IFrame(src=f\"http://localhost:8080/\", width=600, height=600)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c436164",
   "metadata": {},
   "outputs": [],
   "source": [
    "netron.stop()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d1a4a92-1a19-43c0-8aa4-746fcfa487c1",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
