From ae6ca7e2104d9275cf2df73f259cd51f9cb12c1c Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Tue, 7 Apr 2026 11:02:47 +0700 Subject: [PATCH 1/8] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20compos?= =?UTF-8?q?e.service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- restart_all.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/restart_all.sh b/restart_all.sh index bbb517a..2827cd1 100755 --- a/restart_all.sh +++ b/restart_all.sh @@ -37,10 +37,6 @@ restart_docker_services() { else log "Compose file not found: $COMPOSE_FILE" fi - - if ${SUDO[@]} systemctl list-unit-files dronedetector-compose.service >/dev/null 2>&1; then - ${SUDO[@]} systemctl restart dronedetector-compose.service || true - fi } restart_sdr_services() { From 1bd2b5207efec2d0b27cf5b57b33ac856a142c3f Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Tue, 7 Apr 2026 16:18:27 +0700 Subject: [PATCH 2/8] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B1=D0=B0=D0=B3=20=D1=81=20=D0=BF=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D0=B5=D0=BB=D0=B5=D0=BC=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B1=D0=B5=D1=81=D0=BA=D0=BE=D0=BD=D0=B5=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D1=8B=20sen?= =?UTF-8?q?ding=5Fdata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/docker/docker-compose.yml | 4 ++ src/server_to_master.py | 109 ++++++++++++++++++------------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 197b849..50038db 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -18,8 +18,12 @@ services: volumes: - ../../.env:/app/.env:ro - ../../runtime:/app/runtime + - ../../src:/app/src + - ../../common:/app/common networks: - dronedetector-net + extra_hosts: + - "host.docker.internal:host-gateway" dronedetector-nn-server: container_name: dronedetector-nn-server diff --git a/src/server_to_master.py b/src/server_to_master.py index 450b022..6053254 100644 --- a/src/server_to_master.py +++ b/src/server_to_master.py @@ -73,7 +73,6 @@ send_to_jammer_flag = as_bool(os.getenv('send_to_jammer_flag', '0')) latitude = float(os.getenv('latitude')) longitude = float(os.getenv('longitude')) -i = 0 flag = 0 max_len_bulk = 1 bulk_data = [] @@ -98,6 +97,32 @@ freqs_alarm = {freq: 0 for freq in freqs} # 4. Добавить print, только если deub_module_flag. +def ensure_sending_data_task(): + global sending_data_task + + if sending_data_task is None or sending_data_task.done(): + sending_data_task = asyncio.create_task(sending_data()) + + +async def stop_sending_data_task(): + global sending_data_task + + if sending_data_task is None: + return + + task = sending_data_task + sending_data_task = None + + if task.done(): + return + + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + ############################################################################ # GPS MODULE - INACTIVE ############################################################################ @@ -299,44 +324,40 @@ async def sending_data(): от текущего статуса тревоги (аларм/не аларм). """ - global i global alarm global jammer_event - if i == 0: - while True: - i=1 - print('while true!') - ModuleDataSingleV2 = await agregate_data(deepcopy(data_queue)) - if send_to_master_flag: - print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}') - await send_to_master(ModuleDataSingleV2, flag) - - # Если перед отправкой на мастер все было чисто, то ждем 60 сек. - # Если во время этих 60 сек. пришел пакет с алармом, то рассматриваем ситуации: - if not alarm: - for i in range(passive_interval_to_send, 0, -1): - print('ТАЙМЕР ', i) - await asyncio.sleep(1) - if alarm: - break - - # Если стоит флаг отправить данные на джеммер и при этом еще не был получен ивент на глушилку, то - # отправляем на джеммер данные. - elif alarm and send_to_jammer_flag and not jammer_event: - if await send_jam_server_alarm(): - print('Отправили на сервис подавления и все дошло успешно') - else: - print('Не смогли отправить на сервис подавления') - - # Сюда почему-то не заходит и вообще функция не подает признаков жизни после запуска подваителя(( - if alarm and jammer_event: - print('ПОДАВИТЕЛЬ РАБОТАЕТ РАЗБЕГАЙСЯ ААААААААААААААААААА') - - # В случае аларма ждем секунду перед новой отправкой данных. - if alarm: - await asyncio.sleep(active_interval_to_send) - i = 0 + while True: + print('while true!') + ModuleDataSingleV2 = await agregate_data(deepcopy(data_queue)) + if send_to_master_flag: + print(f'На Мастер будет отправлена следующая информация: {ModuleDataSingleV2}') + await send_to_master(ModuleDataSingleV2, flag) + + # Если перед отправкой на мастер все было чисто, то ждем 60 сек. + # Если во время этих 60 сек. пришел пакет с алармом, то рассматриваем ситуации: + if not alarm: + for countdown in range(passive_interval_to_send, 0, -1): + print('ТАЙМЕР ', countdown) + await asyncio.sleep(1) + if alarm: + break + + # Если стоит флаг отправить данные на джеммер и при этом еще не был получен ивент на глушилку, то + # отправляем на джеммер данные. + elif alarm and send_to_jammer_flag and not jammer_event: + if await send_jam_server_alarm(): + print('Отправили на сервис подавления и все дошло успешно') + else: + print('Не смогли отправить на сервис подавления') + + # Сюда почему-то не заходит и вообще функция не подает признаков жизни после запуска подваителя(( + if alarm and jammer_event: + print('ПОДАВИТЕЛЬ РАБОТАЕТ РАЗБЕГАЙСЯ ААААААААААААААААААА') + + # В случае аларма ждем секунду перед новой отправкой данных. + if alarm: + await asyncio.sleep(active_interval_to_send) @app.post('/waterfall') @@ -410,10 +431,8 @@ async def jammer_active(): global jammer_event global freqs_alarm - global sending_data_task - if sending_data_task is not None: - sending_data_task.cancel() + await stop_sending_data_task() freqs_alarm = {freq: 0 for freq in freqs} jammer_event = True @@ -437,11 +456,10 @@ async def jammer_deactive(): global jammer_event global alarm - global sending_data_task alarm = False jammer_event = False set_jammer_active(False) - sending_data_task = asyncio.create_task(sending_data()) + ensure_sending_data_task() print('ОТКЛЮАЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААААА!!!!') print('-' * 20) @@ -493,9 +511,9 @@ async def jam_server(): if data_from_jam_server['type'] == 'run': alarm_status = (data_from_jam_server['data'])['state'] print(alarm_status) - if alarm_status: + if alarm_status and not jammer_event: await jammer_active() - else: + elif not alarm_status and jammer_event: await jammer_deactive() except Exception as e: jam_server_connect = None @@ -511,14 +529,13 @@ async def startup_event(): Запускаем параллельно задачи jam_server и sending_data. """ - global sending_data_task set_jammer_active(False) asyncio.create_task(jam_server()) - sending_data_task = asyncio.create_task(sending_data()) + ensure_sending_data_task() if __name__ == '__main__': import uvicorn # update_gps_coordinates() register_module() # Регистрация модуля на сервере - uvicorn.run(app, host=lochost, port=int(locport)) \ No newline at end of file + uvicorn.run(app, host=lochost, port=int(locport)) From bd09615b552de3245613ee446f6c7c02cc0a2027 Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Thu, 9 Apr 2026 11:19:32 +0700 Subject: [PATCH 3/8] =?UTF-8?q?=D0=A1=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D0=B4=D0=B2=D1=83=D1=85=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- train_scripts/Training_models 2pic.ipynb | 674 ++++++++++++++++++ ...s_1.2.ipynb => Training_models 3pic.ipynb} | 0 2 files changed, 674 insertions(+) create mode 100644 train_scripts/Training_models 2pic.ipynb rename train_scripts/{Training_models_1.2.ipynb => Training_models 3pic.ipynb} (100%) diff --git a/train_scripts/Training_models 2pic.ipynb b/train_scripts/Training_models 2pic.ipynb new file mode 100644 index 0000000..3306d95 --- /dev/null +++ b/train_scripts/Training_models 2pic.ipynb @@ -0,0 +1,674 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5a13ad6b-56c9-4381-b376-1765f6dd7553", + "metadata": { + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Импортирование библиотек" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "7311cb4a-5bf3-4268-b431-43eea10e9ed6", + "metadata": { + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cuda\n" + ] + }, + { + "data": { + "text/plain": [ + "1462" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "from torch.utils.data import Dataset, DataLoader\n", + "from torch import default_generator, randperm\n", + "from torch.utils.data.dataset import Subset\n", + "import torchvision.transforms as transforms\n", + "from torchvision.io import read_image\n", + "from importlib import import_module\n", + "import matplotlib.pyplot as plt\n", + "from torchvision import models\n", + "import torch, torchvision\n", + "from pathlib import Path\n", + "from PIL import Image\n", + "import torch.nn as nn\n", + "from tqdm import tqdm\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib\n", + "import os, shutil\n", + "import mlconfig\n", + "import random\n", + "import shutil\n", + "import timeit\n", + "import copy\n", + "import time\n", + "import cv2\n", + "import csv\n", + "import sys\n", + "import io\n", + "import gc\n", + "\n", + "plt.rcParams[\"savefig.bbox\"] = 'tight'\n", + "torch.manual_seed(1)\n", + "#matplotlib.use('Agg')\n", + "device = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + "print(device)\n", + "torch.cuda.empty_cache()\n", + "cv2.destroyAllWindows()\n", + "gc.collect()" + ] + }, + { + "cell_type": "markdown", + "id": "384de097-82c6-41f5-bda9-b2f54bc99593", + "metadata": { + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Подготовка и обучение детектирование" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "46e4dc99-6994-4fee-a32e-f3983bd991bd", + "metadata": {}, + "outputs": [], + "source": [ + "def prepare_and_learning_detection(num_classes, num_samples, path_dataset, model_name, config_name, model,selected_freq):\n", + " num_samples_per_class = num_samples // num_classes\n", + "\n", + " #----------Создаём папку для сохранения результатов обучения--------------\n", + " os.makedirs(\"models\", exist_ok=True)\n", + " ind = 1\n", + " while True:\n", + " if os.path.exists(\"models/\" + model_name + str(ind)):\n", + " ind += 1\n", + " else:\n", + " os.mkdir(\"models/\" + model_name + str(ind))\n", + " path_res = \"models/\" + model_name + str(ind) + '/'\n", + " break\n", + " \n", + " #----------Создаём файл dataset.csv для обучения--------------\n", + " \n", + " pd_columns = ['file_name']\n", + " df = pd.DataFrame(columns=pd_columns)\n", + " \n", + " subdirs = os.listdir(path_dataset)\n", + " \n", + " for subdir in subdirs:\n", + " freq_dir = os.path.join(path_dataset, subdir, str(selected_freq)+\"_jpg\")\n", + " if not os.path.isdir(freq_dir):\n", + " print(\"Error1\")\n", + " continue\n", + " \n", + " files_k=[f for f in os.listdir(freq_dir)]\n", + " print(len(files_k))\n", + " \n", + " files = [\n", + " f for f in os.listdir(freq_dir)\n", + " if os.path.isfile(os.path.join(freq_dir, f)) and f.endswith('imag.png')\n", + " ]\n", + " num_samples_per_class = min(num_samples_per_class, len(files))\n", + " print(f\"num_samples per class {subdir} is {num_samples_per_class}\")\n", + "\n", + " for subdir in subdirs:\n", + " freq_dir = os.path.join(path_dataset, subdir, str(selected_freq)+\"_jpg\")\n", + " if not os.path.isdir(freq_dir):\n", + " print(\"Error1\")\n", + " continue\n", + "\n", + " files = [\n", + " f for f in os.listdir(freq_dir)\n", + " if os.path.isfile(os.path.join(freq_dir, f)) and f.endswith('imag.png')\n", + " ]\n", + " random.shuffle(files)\n", + " files_to_process = files[:num_samples_per_class]\n", + "\n", + " for file in files_to_process:\n", + " row = pd.DataFrame({\n", + " pd_columns[0]: [str(os.path.join(freq_dir, file))]\n", + " })\n", + " df = pd.concat([df, row], ignore_index=True)\n", + "\n", + " dataset_csv_path = os.path.join(path_res, 'dataset.csv')\n", + " df.to_csv(dataset_csv_path, index=False)\n", + "\n", + " if not os.path.exists(dataset_csv_path):\n", + " raise RuntimeError(f'dataset.csv was not created: {dataset_csv_path}')\n", + " #----------Импортируем параметры для обучения--------------\n", + " \n", + " def load_function(attr):\n", + " module_, func = attr.rsplit('.', maxsplit=1)\n", + " return getattr(import_module(module_), func)\n", + " \n", + " config = mlconfig.load('config_' + config_name + '.yaml')\n", + " \n", + " #----------Создаём класс датасета--------------\n", + " \n", + " class MyDataset(Dataset):\n", + " def __init__(self, path_dataset, csv_file):\n", + " data=[]\n", + " with open(os.path.join(path_dataset, csv_file), newline='') as csvfile:\n", + " reader = csv.reader(csvfile, delimiter=' ', quotechar='|')\n", + " for row in list(reader)[1:]:\n", + " row = str(row)\n", + " data.append(row[2: len(row)-2])\n", + " self.sig_filenames = data\n", + " self.path_dataset = path_dataset\n", + " \n", + " def __len__(self):\n", + " return len(self.sig_filenames)\n", + " \n", + " def __getitem__(self, idx):\n", + " base = os.path.splitext(self.sig_filenames[idx])[0]\n", + " if base[-4:]==\"real\":\n", + " image_real = np.asarray(cv2.split(cv2.imread(base + '.png')), dtype=np.float32)\n", + " image_imag = np.asarray(cv2.split(cv2.imread(base[:-4] + 'imag.png')), dtype=np.float32)\n", + " if base[-4:]==\"imag\":\n", + " image_real = np.asarray(cv2.split(cv2.imread(base[:-4] + 'real.png')), dtype=np.float32)\n", + " image_imag = np.asarray(cv2.split(cv2.imread(base + '.png')), dtype=np.float32)\n", + " \n", + " \n", + " \n", + " if 'drone' in list(self.sig_filenames[idx].split('/')):\n", + " label = torch.tensor(0)\n", + " if 'noise' in list(self.sig_filenames[idx].split('/')):\n", + " label = torch.tensor(1)\n", + " return image_real, image_imag, label\n", + " \n", + " #----------Создаём датасет--------------\n", + " \n", + " dataset = MyDataset(path_dataset=path_res, csv_file='dataset.csv')\n", + " train_set, valid_set = torch.utils.data.random_split(dataset, [0.7, 0.3], generator=torch.Generator().manual_seed(42))\n", + " batch_size = config.batch_size\n", + " train_dataloader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True, drop_last=True)\n", + " valid_dataloader = torch.utils.data.DataLoader(valid_set, batch_size=batch_size, shuffle=True, drop_last=True)\n", + " \n", + " dataloaders = {}\n", + " dataloaders['train'] = train_dataloader\n", + " dataloaders['val'] = valid_dataloader\n", + " dataset_sizes = {}\n", + " dataset_sizes['train'] = len(train_set)\n", + " dataset_sizes['val'] = len(valid_set)\n", + "\n", + " #----------Обучаем модель--------------\n", + "\n", + " val_loss = []\n", + " val_acc = []\n", + " train_loss = []\n", + " train_acc = []\n", + " epochs = config.epoch\n", + " \n", + " best_acc = 0.0\n", + " best_model = copy.deepcopy(model.state_dict())\n", + " limit = config.limit\n", + " epoch_limit = epochs\n", + " \n", + " start = timeit.default_timer()\n", + " for epoch in range(1, epochs+1):\n", + " print(f\"Epoch : {epoch}\\n\")\n", + " dataloader = None\n", + " \n", + " for phase in ['train', 'val']:\n", + " running_loss = 0.0\n", + " running_corrects = 0\n", + " \n", + " for (img1, img2, label) in tqdm(dataloaders[phase]):\n", + " img1, img2, label = img1.to(device), img2.to(device), label.to(device)\n", + " optimizer.zero_grad()\n", + " \n", + " with torch.set_grad_enabled(phase == 'train'):\n", + " output = model([img1, img2])\n", + " _, pred = torch.max(output.data, 1)\n", + " loss = criterion(output, label)\n", + " if phase=='train' :\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " running_loss += loss.item() * 3 * img1.size(0)\n", + " running_corrects += torch.sum(pred == label.data)\n", + " \n", + " epoch_loss = running_loss / dataset_sizes[phase]\n", + " epoch_acc = running_corrects.double() / dataset_sizes[phase]\n", + " \n", + " print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))\n", + " \n", + " if phase=='train' :\n", + " train_loss.append(epoch_loss)\n", + " train_acc.append(epoch_acc)\n", + " else :\n", + " val_loss.append(epoch_loss)\n", + " val_acc.append(epoch_acc)\n", + " if val_acc[-1] > best_acc :\n", + " ind_limit = 0\n", + " best_acc = val_acc[-1]\n", + " best_model = copy.deepcopy(model.state_dict())\n", + " torch.save(best_model, path_res + model_name + '.pth')\n", + " else:\n", + " ind_limit += 1\n", + " \n", + " if ind_limit >= limit:\n", + " break\n", + " \n", + " if ind_limit >= limit:\n", + " epoch_limit = epoch\n", + " break\n", + " \n", + " print()\n", + " \n", + " end = timeit.default_timer()\n", + " print(f\"Total time elapsed = {end - start} seconds\")\n", + " epoch_limit += 1\n", + " \n", + " #----------Вывод графиков и сохранение результатов обучения--------------\n", + " \n", + " train_acc = np.asarray(list(map(lambda x: x.item(), train_acc)))\n", + " val_acc = np.asarray(list(map(lambda x: x.item(), val_acc)))\n", + " \n", + " np.save(path_res+'train_acc.npy', train_acc)\n", + " np.save(path_res+'val_acc.npy', val_acc)\n", + " np.save(path_res+'train_loss.npy', train_loss)\n", + " np.save(path_res+'val_loss.npy', val_loss)\n", + " \n", + " plt.figure()\n", + " plt.plot(range(1,epoch_limit), train_loss, color='blue')\n", + " plt.plot(range(1,epoch_limit), val_loss, color='red')\n", + " plt.xlabel('Epoch')\n", + " plt.ylabel('Loss') \n", + " \n", + " plt.title('Loss Curve')\n", + " plt.legend(['Train Loss', 'Validation Loss'])\n", + " plt.show()\n", + " plt.clf()\n", + " plt.cla()\n", + " plt.close()\n", + " \n", + " plt.figure()\n", + " plt.plot(range(1,epoch_limit), train_acc, color='blue')\n", + " plt.plot(range(1,epoch_limit), val_acc, color='red')\n", + " plt.xlabel('Epoch')\n", + " plt.ylabel('Accuracy')\n", + " plt.title('Accuracy Curve')\n", + " plt.legend(['Train Accuracy', 'Validation Accuracy'])\n", + " plt.show()\n", + " \n", + " plt.clf()\n", + " plt.cla()\n", + " plt.close()\n", + " torch.cuda.empty_cache()\n", + " cv2.destroyAllWindows()\n", + " del model\n", + " gc.collect()\n", + "\n", + " return path_res, model_name" + ] + }, + { + "cell_type": "markdown", + "id": "93c136ee", + "metadata": {}, + "source": [ + "### Ensemble" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "52e8d4c5", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/sibscience-4/from_ssh/DroneDetector/.venv-train/lib/python3.12/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/home/sibscience-4/from_ssh/DroneDetector/.venv-train/lib/python3.12/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6900\n", + "num_samples per class noise is 1725\n", + "2530\n", + "num_samples per class drone is 1265\n", + "Epoch : 1\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 442/442 [01:58<00:00, 3.75it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.1137 Acc: 0.9876\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 189/189 [00:26<00:00, 7.13it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 0.0101 Acc: 0.9960\n", + "\n", + "Epoch : 2\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 442/442 [01:51<00:00, 3.95it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.0219 Acc: 0.9977\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 189/189 [00:24<00:00, 7.72it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 0.0045 Acc: 0.9960\n", + "\n", + "Epoch : 3\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 442/442 [01:52<00:00, 3.94it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.0027 Acc: 0.9983\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 189/189 [00:24<00:00, 7.73it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 0.0013 Acc: 0.9960\n", + "\n", + "Epoch : 4\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 442/442 [01:52<00:00, 3.93it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.0009 Acc: 0.9983\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 189/189 [00:24<00:00, 7.60it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 0.0006 Acc: 0.9960\n", + "\n", + "Epoch : 5\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 442/442 [01:51<00:00, 3.96it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.0759 Acc: 0.9915\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 189/189 [00:24<00:00, 7.69it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 0.0037 Acc: 0.9960\n", + "\n", + "Epoch : 6\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 442/442 [01:52<00:00, 3.93it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train Loss: 0.0026 Acc: 0.9983\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 189/189 [00:22<00:00, 8.32it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "val Loss: 0.0019 Acc: 0.9960\n", + "Total time elapsed = 826.4518795179974 seconds\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkkAAAHHCAYAAACr0swBAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAcQ1JREFUeJzt3Xl8TGf7P/DPZN8jJLIhIZZYo4LYVYVYaxdKxb4lnqrSVmlt9dB+Ve2U2veqpao/0oh9qbXUvqZEiIglG7JM7t8f55lJRiYkTHJm+bxfr3k5c3LPzDVTNVfuc93XrRBCCBARERGRBjO5AyAiIiLSR0ySiIiIiLRgkkRERESkBZMkIiIiIi2YJBERERFpwSSJiIiISAsmSURERERaMEkiIiIi0oJJEhEREZEWTJKIiIiItGCSRERvtGjRIigUCgQFBckdikF6+PAhxo4dC39/f9jZ2cHe3h6BgYH49ttv8ezZM7nDI6J8KLh3GxG9SePGjXH//n38+++/uHHjBipWrCh3SAbj1KlTaNeuHVJTU9G3b18EBgYCAE6fPo1NmzahUaNG+PPPP2WOkoi0YZJERK8VExODChUqYNu2bRg2bBjCw8MxadIkucPSKi0tDfb29nKHofbs2TPUqFEDWVlZOHDgAPz9/TV+/vDhQyxbtgwTJ05859fSt/dOZAx4uY2IXmv9+vVwcXFB+/bt0b17d6xfv17ruGfPnuHTTz+Fr68vrK2tUaZMGfTr1w+JiYnqMS9fvsTkyZNRuXJl2NjYwNPTE127dsWtW7cAAAcOHIBCocCBAwc0nvvff/+FQqHAqlWr1Of69+8PBwcH3Lp1C+3atYOjoyP69OkDADh8+DB69OiBcuXKwdraGmXLlsWnn36KFy9e5In76tWr6NmzJ9zc3GBra4sqVapgwoQJAID9+/dDoVBg+/bteR63YcMGKBQKHD9+PN/P7qeffkJcXBxmz56dJ0ECAHd3d40ESaFQYPLkyXnG+fr6on///ur7q1atgkKhwMGDBzFy5EiULl0aZcqUwa+//qo+ry0WhUKBixcvarz37t27o2TJkrCxsUHdunWxc+fOfN8PkamxkDsAItJv69evR9euXWFlZYXevXtj8eLFOHXqFOrVq6cek5qaiqZNm+LKlSsYOHAg6tSpg8TEROzcuRP37t2Dq6srlEolOnTogOjoaPTq1QuffPIJUlJSEBUVhYsXL8LPz6/QsWVlZSEkJARNmjTBrFmzYGdnBwDYsmULnj9/jhEjRqBUqVI4efIk5s+fj3v37mHLli3qx//zzz9o2rQpLC0tMXToUPj6+uLWrVv4/fffMX36dLz//vsoW7Ys1q9fjy5duuT5XPz8/NCwYcN849u5cydsbW3RvXv3Qr+3ghg5ciTc3NzwzTffIC0tDe3bt4eDgwN++eUXNG/eXGPs5s2bUb16ddSoUQMAcOnSJTRu3Bje3t748ssvYW9vj19++QWdO3fG1q1b87xfIpMkiIjycfr0aQFAREVFCSGEyM7OFmXKlBGffPKJxrhvvvlGABDbtm3L8xzZ2dlCCCFWrFghAIjZs2fnO2b//v0CgNi/f7/Gz2NiYgQAsXLlSvW5sLAwAUB8+eWXeZ7v+fPnec7NmDFDKBQKcefOHfW5Zs2aCUdHR41zueMRQojx48cLa2tr8ezZM/W5hIQEYWFhISZNmpTndXJzcXERAQEBrx2TGwCtz+nj4yPCwsLU91euXCkAiCZNmoisrCyNsb179xalS5fWOP/gwQNhZmYmpk6dqj7XsmVLUbNmTfHy5Uv1uezsbNGoUSNRqVKlAsdMZMx4uY2I8rV+/Xq4u7ujRYsWAKTLQaGhodi0aROUSqV63NatWxEQEKB19kGhUKjHuLq6YtSoUfmOeRsjRozIc87W1lZ9nJaWhsTERDRq1AhCCPz9998AgEePHuHQoUMYOHAgypUrl288/fr1Q3p6On799Vf1uc2bNyMrKwt9+/Z9bWzJyclwdHR8q/dVEEOGDIG5ubnGudDQUCQkJGhcsvz111+RnZ2N0NBQAMCTJ0+wb98+9OzZEykpKUhMTERiYiIeP36MkJAQ3LhxA3FxcUUWN5GhYJJERFoplUps2rQJLVq0QExMDG7evImbN28iKCgIDx8+RHR0tHrsrVu31Jdx8nPr1i1UqVIFFha6u8pvYWGBMmXK5Dl/9+5d9O/fHyVLloSDgwPc3NzUl5+SkpIAALdv3waAN8bt7++PevXqadRirV+/Hg0aNHjjKj8nJyekpKQU6j0VRvny5fOca9OmDZydnbF582b1uc2bN6N27dqoXLkyAODmzZsQQuDrr7+Gm5ubxk1VlJ+QkFBkcRMZCtYkEZFW+/btw4MHD7Bp0yZs2rQpz8/Xr1+P1q1b6/Q185tRyj1rlZu1tTXMzMzyjG3VqhWePHmCL774Av7+/rC3t0dcXBz69++P7OzsQsfVr18/fPLJJ7h37x7S09Px119/YcGCBW98nL+/P86dO4eMjAxYWVkV+nVV8nv/uWfMVKytrdG5c2ds374dixYtwsOHD3H06FH897//VY9RfQZjx45FSEiI1udmmwciJklElI/169ejdOnSWLhwYZ6fbdu2Ddu3b8eSJUtga2sLPz8/jVVT2vj5+eHEiRPIzMyEpaWl1jEuLi4AkKfB4p07dwoc94ULF3D9+nWsXr0a/fr1U5+PiorSGFehQgUAeGPcANCrVy+MGTMGGzduxIsXL2Bpaam+dPU6HTt2xPHjx7F161b07t37jeNdXFzyvPeMjAw8ePDgjY/NLTQ0FKtXr0Z0dDSuXLkCIYRGvKr3bmlpieDg4EI9N5Ep4eU2IsrjxYsX2LZtGzp06IDu3bvnuUVERCAlJUW9XLxbt244f/681qXy4n+t2Lp164bExEStMzCqMT4+PjA3N8ehQ4c0fr5o0aICx66q0RG5WsAJITB37lyNcW5ubmjWrBlWrFiBu3fvao1HxdXVFW3btsW6deuwfv16tGnTBq6urm+MZfjw4fD09MRnn32G69ev5/l5QkICvv32W/V9Pz+/PO996dKl+c4k5Sc4OBglS5bE5s2bsXnzZtSvX1/j0lzp0qXx/vvv46efftKagD169KhQr0dkrDiTRER57Ny5EykpKfjwww+1/rxBgwZwc3PD+vXrERoainHjxuHXX39Fjx49MHDgQAQGBuLJkyfYuXMnlixZgoCAAPTr1w9r1qzBmDFjcPLkSTRt2hRpaWnYu3cvRo4ciU6dOsHZ2Rk9evTA/PnzoVAo4Ofnh127dhWqPsbf3x9+fn4YO3Ys4uLi4OTkhK1bt+Lp06d5xs6bNw9NmjRBnTp1MHToUJQvXx7//vsv/vjjD5w7d05jbL9+/dRL+adNm1agWFxcXLB9+3a0a9cOtWvX1ui4ffbsWWzcuFGjhcDgwYMxfPhwdOvWDa1atcL58+cRGRlZoIQsN0tLS3Tt2hWbNm1CWloaZs2alWfMwoUL0aRJE9SsWRNDhgxBhQoV8PDhQxw/fhz37t3D+fPnC/WaREZJvoV1RKSvOnbsKGxsbERaWlq+Y/r37y8sLS1FYmKiEEKIx48fi4iICOHt7S2srKxEmTJlRFhYmPrnQkhL8ydMmCDKly8vLC0thYeHh+jevbu4deuWesyjR49Et27dhJ2dnXBxcRHDhg0TFy9e1NoCwN7eXmtsly9fFsHBwcLBwUG4urqKIUOGiPPnz+d5DiGEuHjxoujSpYsoUaKEsLGxEVWqVBFff/11nudMT08XLi4uwtnZWbx48aIgH6Pa/fv3xaeffioqV64sbGxshJ2dnQgMDBTTp08XSUlJ6nFKpVJ88cUXwtXVVdjZ2YmQkBBx8+bNfFsAnDp1Kt/XjIqKEgCEQqEQsbGxWsfcunVL9OvXT3h4eAhLS0vh7e0tOnToIH799ddCvT8iY8VtSYiICiArKwteXl7o2LEjli9fLnc4RFQMWJNERFQAO3bswKNHjzSKwYnIuHEmiYjoNU6cOIF//vkH06ZNg6urK86ePSt3SERUTDiTRET0GosXL8aIESNQunRprFmzRu5wiKgYcSaJiIiISAvOJBERERFpwSSJiIiISAs2k3xL2dnZuH//PhwdHd9pB3MiIiIqPkIIpKSkwMvLK8/ej69ikvSW7t+/j7Jly8odBhEREb2F2NhYlClT5rVjmCS9JUdHRwDSh+zk5CRzNERERFQQycnJKFu2rPp7/HWYJL0l1SU2JycnJklEREQGpiClMizcJiIiItKCSRIRERGRFkySiIiIiLRgkkRERESkBZMkIiIiIi2YJBERERFpwSSJiIiISAsmSURERERaMEkiIiIi0oJJEhEREZEWTJKIiIiItGCSRERERKQFN7gloiKVlAQ8eyZ3FKbBzQ2ws5M7CqJ39/gxcPs2UK+evHEwSSIinUlMBM6ezbmdOSP9Q0fFo2RJ4NIlwMND7kiI3s1PPwETJgCDBwPLlskXB5MkInorDx5oJkRnzwJ372ofa20NKBTFG5+pycgAnjyRvlwmTZI7GqK3l5UFLF4sHTdtKm8sTJKI6LWEAGJj884QxcdrH1+pElCnTs7tvfeAUqWKN2ZTtHEj8NFHwJIlwPjxgJWV3BERvZ3ffgPu3ZMuH4eGyhsLkyQiUhNCujz26gxRYmLesWZmgL+/ZkJUuzbg7FzsYROAbt2ky2zx8cDWrUDv3nJHRPR25s+X/hw6VJqFlhOTJCITlZ0N3LghzQrlToiSkvKOtbAAqlfXTIgCAgB7++KPm7SzsgKGDwcmTwYWLGCSRIbpn3+AgwcBc3Pp77PcmCQRmYCsLODqVc2E6Nw5IDU171grK6BWLc2EqGZNwMam2MOmQho2DJg+HTh2TPpvXKeO3BERFc6CBdKfXboAZcrIGwugB32SFi5cCF9fX9jY2CAoKAgnT57Md2xmZiamTp0KPz8/2NjYICAgAHv27NEYk5KSgtGjR8PHxwe2trZo1KgRTp06pTEmNTUVERERKFOmDGxtbVGtWjUsWbKkSN4fUXHLyAD+/hv4+Wdg5EigQQPA0VFKdPr3B+bNA44ckRIkW1ugYUMgPBxYvlx6XEoKcOqUVAA8bJi0BJcJkmHw8AB69JCOVZcsiAzF06fAunXS8ahR8saiJmS0adMmYWVlJVasWCEuXbokhgwZIkqUKCEePnyodfznn38uvLy8xB9//CFu3bolFi1aJGxsbMTZs2fVY3r27CmqVasmDh48KG7cuCEmTZoknJycxL1799RjhgwZIvz8/MT+/ftFTEyM+Omnn4S5ubn47bffChx7UlKSACCSkpLe/gMgekfPnwtx4oQQixYJMXiwEHXqCGFpKYRUXaR5c3QUolkzIUaPFmLNGiEuXhQiM1Pud0C6duyY9N/b2lqIhAS5oyEquFmzpL+7tWoJkZ1ddK9TmO9vWZOk+vXri/DwcPV9pVIpvLy8xIwZM7SO9/T0FAsWLNA417VrV9GnTx8hhBDPnz8X5ubmYteuXRpj6tSpIyZMmKC+X716dTF16tTXjnkTJklU3FJShDhyRIi5c4UICxOiZk0hzM21J0QuLkK0bCnEuHFCbNwoxPXrQiiVcr8DKg7Z2UIEBkp/D/77X7mjISqYrCwhypeX/t4uXVq0r1WY72/ZapIyMjJw5swZjB8/Xn3OzMwMwcHBOH78uNbHpKenw+aVeX9bW1scOXIEAJCVlQWlUvnaMQDQqFEj7Ny5EwMHDoSXlxcOHDiA69ev48cff9TV2yN6J8+eSTVDuWuIrl2TUqBXubkBgYGaNUS+vuxLZKoUCulSRf/+Uq+ZceOkwnsiffb//h8QEwO4uAB9+sgdTQ7Z/tdJTEyEUqmEu7u7xnl3d3dcvXpV62NCQkIwe/ZsNGvWDH5+foiOjsa2bdugVCoBAI6OjmjYsCGmTZuGqlWrwt3dHRs3bsTx48dRsWJF9fPMnz8fQ4cORZkyZWBhYQEzMzMsW7YMzZo1yzfe9PR0pKenq+8nJye/y9snUktMlGqBVP2Hzp4Fbt3SPtbbWzMZCgwEvLyYEJGm0FBg7Fipv9XOnUDXrnJHRPR6qhq6QYP0a2sdg/r9Yu7cuRgyZAj8/f2hUCjg5+eHAQMGYMWKFeoxa9euxcCBA+Ht7Q1zc3PUqVMHvXv3xpkzZ9Rj5s+fj7/++gs7d+6Ej48PDh06hPDwcHh5eSE4OFjra8+YMQNTpkwp8vdIxi0+XrMh4+u6VPv6aiZEdeoAr/xOQaSVjQ0wZAgwY4b05cMkifTZ1atAVJT0y97IkXJHo0khhLYJ/KKXkZEBOzs7/Prrr+jcubP6fFhYGJ49e4bffvst38e+fPkSjx8/hpeXF7788kvs2rULly5d0hiTlpaG5ORkeHp6IjQ0FKmpqfjjjz/w4sULODs7Y/v27Wjfvr16/ODBg3Hv3r08q+VUtM0klS1bFklJSXBycnrLT4GMlRBSx9hXE6IHD7SPz92lOjBQ6lJdsmTxxkzGJTYWKF8eUCql3jM1a8odEZF2o0ZJS/8//FDqtl3UkpOT4ezsXKDvb9lmkqysrBAYGIjo6Gh1kpSdnY3o6GhERES89rE2Njbw9vZGZmYmtm7dip49e+YZY29vD3t7ezx9+hSRkZH4/vvvAUhtBDIzM2Fmptn9wNzcHNnZ2fm+prW1Nazlbv1JekkI6Vr6qwlRQbtUv/cewDybdK1sWaBzZ6n79sKF0nYlRPomORlYtUo6fsNXvyxkvdw2ZswYhIWFoW7duqhfvz7mzJmDtLQ0DBgwAADQr18/eHt7Y8aMGQCAEydOIC4uDrVr10ZcXBwmT56M7OxsfP755+rnjIyMhBACVapUwc2bNzFu3Dj4+/urn9PJyQnNmzfHuHHjYGtrCx8fHxw8eBBr1qzB7Nmzi/9DIIOi6lKdOyH6+2+p0PpVr3apDgyUmjSySzUVl1GjpCRp7Vrp0puLi9wREWlavVrq2ebvD+RT7SIrWZOk0NBQPHr0CN988w3i4+NRu3Zt7NmzR13MfffuXY0Zn5cvX2LixIm4ffs2HBwc0K5dO6xduxYlSpRQj0lKSsL48eNx7949lCxZEt26dcP06dNhaWmpHrNp0yaMHz8effr0wZMnT+Dj44Pp06djuD70QCe9oepSnXvLjr//LliX6sBAoEYNNmEkeTVrJl1mu3ABWLkSGDNG7oiIcmRn53TYjojQzwUostUkGbrCXNMk/ZeRAVy6pJkQnT8PvHiRd6ytrbSRa+6EqFo1IFceTqQ3li6VOqdXqABcvy7tiUWkD/78EwgJkXYEiIuT/iwOBlGTRCQnIYANG6SNFM+elX7TzsjIO87RUaoZyp0QVanCLxoyHH36AF98Ady+DezeDXToIHdERBLVsv8BA4ovQSosJklkkiIjgb59Nc+5uGgmQ3XqAH5+UrE1kaGyt5d6z/zwg3Rpg0kS6YPbt4E//pCOw8PljeV1mCSRSfr9d+nPFi2ka+F16gA+Pvp5TZzoXY0cCcyeLf1ycO2aNBtKJKeFC6UZ/ZAQoHJluaPJH39HJpMjhHTZAQA+/VRqtMdtPMiYVaiQM4O0cKG8sRClpQGqHtCjRskby5swSSKTc+OG1NfIygr44AO5oyEqHqoeNKtWASkpsoZCJm79eqltip8f0Lat3NG8HpMkMjmqWaRmzdiziExHcLB0mS0lRepNQyQHIXIKtsPD9b/mU8/DI9I91c4zbdrIGwdRcTIzy5lNWrBA+rIiKm4HDwIXL0qb2P6vx7NeY5JEJuXFC+DAAelY36d5iXQtLExaan3tGrB3r9zRkClSzSJ9/DGQqw+03mKSRCbl4EHg5UtpX6uqVeWOhqh4OToC/ftLx6ovK6LicvcusGOHdKyP+7RpwySJTIqqHqltW65mI9Ok6kmza5fUq4aouCxZIm1F0qKFtG2TIWCSRCaF9Uhk6qpUAVq3lmqSFi2SOxoyFS9fAsuWScf6vuw/NyZJZDJu35b2rrKwAFq2lDsaIvmovqSWLweeP5c3FjINmzYBiYlSqUPHjnJHU3BMkshkqGaRGjcGuCcxmbK2baUGk8+eST1riIpS7mX/I0dKv6gaCiZJZDJy1yMRmTJz85zapPnz2Q6AitZff0kbiVtbA4MHyx1N4TBJIpOQng7s2ycdsx6JSOpRY2cHXLgAHDokdzRkzFSzSB99BLi6yhtLYTFJIpNw+LBUe+HpCdSqJXc0RPJzcQH69pWO2Q6AisqDB8CWLdKxoSz7z41JEpmE3KvauPSfSKL60tqxA4iNlTUUMlI//QRkZQGNGgF16sgdTeExSSKTwHokorxq1gTefx9QKqUeNkS6lJEhJUmAYS37z41JEhm9u3eBy5elvauCg+WOhki/qL68li6VetkQ6crWrUB8vFTm0K2b3NG8HSZJZPRUl9oaNpTqMIgox4cfSr1rEhOBzZvljoaMiarWbfhwwNJS3ljeFpMkMnqqS21c1UaUl4UFMGKEdMx2AKQrZ84Ax49LydHQoXJH8/aYJJFRy8gAoqOlY9YjEWk3ZIjUw+bMGeDECbmjIWOgmkXq0QPw8JA3lnfBJImM2rFjQEoKULo08N57ckdDpJ9cXYHevaVjtgOgd/XokbQNCWC4BdsqTJLIqKnqkUJCpMJtItJO1Q5gyxap2Jbobf38s9TAt25dIChI7mjeDb82yKhx6T9RwQQGSosbMjNzlm0TFVZWFrB4sXQ8apTh96VjkkRG6/594J9/pP9JW7WSOxoi/ae6NLJkiVTPR1RYv/0mNSZ1dQV69pQ7mnfHJImMlupSW/36hrdfEJEcunWTimzj44Ft2+SOhgyRqqZt6FDAxkbeWHSBSRIZrdxbkRDRm1lZST1tABZwU+FduAAcPAiYm+e0lTB0TJLIKGVlAVFR0jHrkYgKbuhQqXfSsWPA2bNyR0OGZMEC6c8uXYAyZeSNRVeYJJFROnECePYMKFVKWmFBRAXj6Sn1tgE4m0QF9/QpsG6ddGzoy/5zY5JERkm1qq11a2nql4gKTvUlt3GjtF0J0ZusWAE8fy5tmty0qdzR6A6TJDJKrEciensNGkgtAdLTpZ43RK+jVAILF0rHxrDsPzcmSWR0Hj6UtlcApCaSRFQ4CkXObNKiRVKNH1F+du8GYmKkDcT79JE7Gt1ikkRG588/pT/r1AHc3eWNhchQhYZKrTNiY4GdO+WOhvSZqnZt0CDAzk7eWHSNSRIZHXbZJnp3NjbSxrcAC7gpf9euSb+YKhTAyJFyR6N7TJLIqCiVQGSkdMx6JKJ3M2KEtPDhwAHg4kW5oyF9pFr236EDUL68vLEUBSZJZFROnwaePAGcnaXiUyJ6e2XLAp07S8eqL0MileRkYNUq6diYlv3nxiSJjIrqUlurVlJDPCJ6N6ovv7VrpV44RCpr1gCpqYC/PxAcLHc0RYNJEhkV1dJ/1iMR6UazZkCNGlIPnJUr5Y6G9EV2ds7sYkSEcS37z41JEhmNxETg5EnpmEv/iXQjdzuAhQulL0eivXulom1HR6BfP7mjKTpMkshoREUBQgC1agHe3nJHQ2Q8+vQBSpQAbt/OuaRNpk214rF/fylRMlZMkshoqP7x5qo2It2yt5d64ABsB0BSsvzHH9JxRIS8sRQ1JklkFLKzc5b+sx6JSPdGjpQuvUVGSpdZyHQtWiTN2oeEAJUryx1N0WKSREbh77+BhATAwQFo1EjuaIiMT4UKQPv20rFqny4yPWlpwPLl0rGxLvvPjUkSGQXVqrbgYMDKSt5YiIyV6ktx1SogJUXWUEgm69cDz55JSbMplDYwSSKjwK1IiIpecDBQpYqUIK1ZI3c0VNyEyKlJCw+XurEbOyZJZPCePgWOH5eOTeE3GyK5mJnlFOouWCB9aZLpOHRI2p7Gzg4YOFDuaIoHkyQyeHv3SoXb1aoB5crJHQ2RcQsLk5Z8X70q/b9HpkM1i/Txx1JLCFPAJIkMnqoeibNIREXP0VFKlAC2AzAlsbHAjh3SsbEv+8+NSRIZNCG4FQlRcVN9Se7aBcTEyBsLFY/FiwGlEnj/fWmbGlPBJIkM2oULwP370jXypk3ljobINFSpArRuLf2SsmiR3NFQUXv5Eli2TDo2hWX/uTFJIoOmWtX2wQeAtbW8sRCZEtWX5fLl0ua3ZLw2b5b2xixbFvjwQ7mjKV6yJ0kLFy6Er68vbGxsEBQUhJOqHUq1yMzMxNSpU+Hn5wcbGxsEBARgj+pay/+kpKRg9OjR8PHxga2tLRo1aoRTp07lea4rV67gww8/hLOzM+zt7VGvXj3cvXtX5++Piha3IiGSR9u2Uq+cp0+l3jlknHIv+x85ErCwkDee4ibr2928eTPGjBmDJUuWICgoCHPmzEFISAiuXbuG0qVL5xk/ceJErFu3DsuWLYO/vz8iIyPRpUsXHDt2DO+99x4AYPDgwbh48SLWrl0LLy8vrFu3DsHBwbh8+TK8/7fr6a1bt9CkSRMMGjQIU6ZMgZOTEy5dugQbG5tiff9aCcFfywooORn4+whgB6BdcwBpckdEZDrMAfxnEPDVBODnucDg3tK2JWRcTp4ArpwBXKyk/8bF/u+snZ28f7GEjOrXry/Cw8PV95VKpfDy8hIzZszQOt7T01MsWLBA41zXrl1Fnz59hBBCPH/+XJibm4tdu3ZpjKlTp46YMGGC+n5oaKjo27fvO8WelJQkAIikpKR3ep48UlOFkFIl3njjjTfeeDPtW2qqbr9jReG+v2W73JaRkYEzZ84gODhYfc7MzAzBwcE4ruoM+Ir09PQ8sz22trY4cuQIACArKwtKpfK1Y7Kzs/HHH3+gcuXKCAkJQenSpREUFIQdqrWN+UhPT0dycrLGjYiIiIyXbJfbEhMToVQq4e7urnHe3d0dV69e1fqYkJAQzJ49G82aNYOfnx+io6Oxbds2KJVKAICjoyMaNmyIadOmoWrVqnB3d8fGjRtx/PhxVKxYEQCQkJCA1NRUzJw5E99++y2+++477NmzB127dsX+/fvRvHlzra89Y8YMTJkyRYefQD7s7IDU1KJ/HQMnBODvD9yLA7Zvk1baEFHxu3gRCGoAmJsBly8DZcrIHRHpyn//C0z/L9AgCIiOlikIOzuZXlhiUCVYc+fOxZAhQ+Dv7w+FQgE/Pz8MGDAAK1asUI9Zu3YtBg4cCG9vb5ibm6NOnTro3bs3zpw5A0CaSQKATp064dNPPwUA1K5dG8eOHcOSJUvyTZLGjx+PMWPGqO8nJyejbNmyun+TCgVgb6/75zUyVy4D1+MAGxugaRsAtnJHRGSaagQB9ZoDBw8Ci9cA06fLHRHpQkYGMH8F8BzAkNEATPRrSbbLba6urjA3N8fDhw81zj98+BAeHh5aH+Pm5oYdO3YgLS0Nd+7cwdWrV+Hg4IAKFSqox/j5+eHgwYNITU1FbGwsTp48iczMTPUYV1dXWFhYoFq1ahrPXbVq1deubrO2toaTk5PGjeSjWtXWvDlgywSJSFaqdgBLl0o9dcjwbd0KxMcDnp5A165yRyMf2ZIkKysrBAYGIjrXHF52djaio6PRsGHD1z7WxsYG3t7eyMrKwtatW9GpU6c8Y+zt7eHp6YmnT58iMjJSPcbKygr16tXDtWvXNMZfv34dPj4+OnhnVBzYZZtIf3TqJPXQSUwEfvlF7mhIF1TL/ocNA6ys5I1FTrJebhszZgzCwsJQt25d1K9fH3PmzEFaWhoGDBgAAOjXrx+8vb0xY8YMAMCJEycQFxeH2rVrIy4uDpMnT0Z2djY+//xz9XNGRkZCCIEqVarg5s2bGDduHPz9/dXPCQDjxo1DaGgomjVrhhYtWmDPnj34/fffceDAgWJ9//R2UlOl3agB9kci0gcWFsCIEcBXX0lfrh9/zHYAhuzMGeD4ccDSUkqSTJrO19YV0vz580W5cuWElZWVqF+/vvjrr7/UP2vevLkICwtT3z9w4ICoWrWqsLa2FqVKlRIff/yxiIuL03i+zZs3iwoVKggrKyvh4eEhwsPDxbNnz/K87vLly0XFihWFjY2NCAgIEDt27ChU3EXWAoDe6PffpZWh5csLkZ0tdzREJIQQCQlCWFtL/28ePy53NPQu+veX/jt+9JHckRSNwnx/K4QQQu5EzRAlJyfD2dkZSUlJrE8qZuHh0n5RI0Zw3ygifdK/P7B6NfDRR+zCbagePZIunaanS7NJDRrIHZHuFeb7W/ZtSYgKQ4icom3WIxHpF1UB95YtUtEvGZ6ff5YSpMBAIChI7mjkxySJDMqNG0BMjFRI2KKF3NEQUW6BgUDDhkBmprTSjQxLVhaweLF0PGoU68oAJklkYFSr2po2BRwc5I2FiPJSzSYtWSL12iHDsXMnEBsLuLoCoaFyR6MfmCSRQeGlNiL91q0b4OEBPHgAbNsmdzRUGKpl/0OHSo16iUkSGZAXLwBVlwYu/SfST1ZWOcvGVV+6pP8uXJD+fTU3B4YPlzsa/cEkiQzGwYNSN9+yZYFXGqYTkR4ZNkzqnXTsGHD2rNzRUEEsWCD92bmz9G8sSZgkkcFQXWpr04YFhUT6zNMT6NFDOlZ9+ZL+evoUWLdOOlbVlJGESRIZDG5FQmQ4VF+2GzZI25WQ/lq5Enj+HKhZE2jWTO5o9AuTJDIIt28D169LU/gtW8odDRG9SYMGUkuA9HSp9w7pJ6USWLhQOuay/7yYJJFBUM0iNW4MsME5kf5TKICICOl40SKpBw/pn927pV9CS5QA+vSROxr9wySJDELueiQiMgy9ekk9d2Jjgd9/lzsa0ka1AnHQIMDOTt5Y9BGTJNJ76enAvn3SMeuRiAyHjQ0wZIh0zHYA+ufaNeDPP6VZv5Ej5Y5GPzFJIr13+LBUVOjpCdSqJXc0RFQYI0ZIvXf27wcuXpQ7GspNVYvUoQNQoYK8segrJkmk91T1SFz6T2R4ypaVeu8AbAegT1JSgFWrpGMu+88fkyTSe6xHIjJsqgLutWulnjwkv9WrpUSpShUgOFjuaPQXkyTSa3fvApcvA2ZmQKtWckdDRG+jeXOgRg3psrlq9oLkk52dM6sXEcEZ+tdhkkR6TXWprUEDwMVF3liI6O0oFDmXdBYulL6kST7R0VLRtqMjEBYmdzT6jUkS6TV22SYyDn36SL14bt3KuYRO8lCtNOzfX0qUKH9MkkhvZWQAe/dKx6xHIjJs9vZSLx6A7QDkdPs2sGuXdKyqFaP8MUkivXX8uFRY6OYG1KkjdzRE9K5GjpQuvUVGStsMUfFbtAgQAggJASpXljsa/cckifSWako+JEQq3CYiw1ahAtC+vXSs6tFDxSctDVi+XDrmLFLB8KuH9BbrkYiMj6qAe+VKaaaYis+GDcCzZ1Kyyn9XC4ZJEuml+/eB8+elqfnWreWOhoh0JThY6s2TkgKsWSN3NKZDiJxasPBwqQs6vRmTJNJLqlmkevWkDTKJyDiYmeVc6lmwQPrypqJ36BBw4YK0ie3AgXJHYziYJJFe4qU2IuPVrx/g4ABcvSr17KGip5pF+vhjqRUDFQyTJNI7WVlAVJR0zKX/RMbHyUnq0QOwHUBxiI0FduyQjsPDZQ3F4DBJIr1z4oRUXFiypHS5jYiMj+qS2++/AzEx8sZi7JYsAZRK4P33gZo15Y7GsDBJIr2jWvrfujWLC4mMVZUq0v/jQki9e6hovHwJLF0qHatWFlLBMUkivcN6JCLToJpNWr5c2vyWdG/zZiAxEShbFvjwQ7mjMTxMkkivPHwInDkjHYeEyBsLERWtdu2A8uWBp0+lHj6kW7mX/Y8YAVhYyBuPIWKSRHrlzz+lP+vUAdzd5Y2FiIqWuXlOIfH8+WwHoGsnTki/dFpbA0OGyB2NYWKSRHpFVY/EVW1EpmHgQKl3zz//AIcPyx2NcVHNIvXuzX5zb4tJEukNpTJnJon1SESmwcUF6NtXOmY7AN2Jjwe2bJGOWbD99pgkkd44fRp4/BhwdgYaNJA7GiIqLqoC7u3bpZ4+9O5++gnIzAQaNZLKF+jtMEkivaFa1daqFQsMiUxJzZpA8+bSbPJPP8kdjeHLyJB6IwE5CSi9HSZJpDdYj0RkulSXhJYulXr70Nvbtk263ObhAXTrJnc0ho1JEumFx4+BkyelYyZJRKanUyepl8+jR8Avv8gdjWFT1XYNHw5YWckbi6FjkkR64c8/peW/NWsC3t5yR0NExc3CQurlA7AdwLs4exY4dgywtASGDZM7GsPHJIn0gupSG1e1EZmuwYOlnj6nT0s9fqjwVLNIPXpIl9vo3TBJItllZwORkdIxL7URmS43N6BXL+l4wQJ5YzFEjx4BGzdKxyzY1g0mSSS7v/8GEhIABwegcWO5oyEiOakKuH/5RSo+poJbvhxITwcCA9lGRVeYJJHsVEv/g4NZZEhk6gIDgYYNpR4/qt3r6c2ysoBFi6TjUaMAhULeeIwFkySSHZf+E1FuqtmkJUuknj/0Zjt3So04XV2B0FC5ozEeTJJIVk+fAsePS8dMkogIkHr7eHgADx5IXbjpzVQF20OHAjY28sZiTJgkkaz27pUKt6tWBXx85I6GiPSBlVXO8nXu5/ZmFy8CBw4A5uZSbyTSHSZJJCtVPRKX/hNRbsOGSb2Tjh6VFndQ/lQrATt3lhpyku4wSSLZCJGTJPFSGxHl5ukp9foBOJv0Ok+fAmvXSseqWi7SHSZJJJsLF4D79wE7O6BpU7mjISJ9o/rS37ABSEyUNxZ9tXIl8Py5tFtBs2ZyR2N8mCSRbFSr2lq0YKEhEeXVoAFQp47U+2f5crmj0T9KJbBwoXTMZf9Fg0kSyYb1SET0OgpFzmzSokVSLyDKsWcPcPs2UKIE8NFHckdjnJgkkSySk4EjR6Rj1iMRUX569ZJ6/9y9C/z+u9zR6BdVrdagQYC9vbyxGCu9SJIWLlwIX19f2NjYICgoCCdPnsx3bGZmJqZOnQo/Pz/Y2NggICAAe1RTEv+TkpKC0aNHw8fHB7a2tmjUqBFOnTqV73MOHz4cCoUCc+bM0dVbojfYt0/6rbBSJcDPT+5oiEhf2dgAQ4ZIxyzgznHtmrTnpUIBjBwpdzTGS/YkafPmzRgzZgwmTZqEs2fPIiAgACEhIUhISNA6fuLEifjpp58wf/58XL58GcOHD0eXLl3wd641ooMHD0ZUVBTWrl2LCxcuoHXr1ggODkZcXFye59u+fTv++usveHl5Fdl7pLzYZZuICmrECMDMDNi/X+oJRDm1SB06ABUqyBuLURMyq1+/vggPD1ffVyqVwsvLS8yYMUPreE9PT7FgwQKNc127dhV9+vQRQgjx/PlzYW5uLnbt2qUxpk6dOmLChAka5+7duye8vb3FxYsXhY+Pj/jxxx8LHHdSUpIAIJKSkgr8GJJkZwtRrpwQgBD/7//JHQ0RGYKuXaV/M4YPlzsS+SUnC+HoKH0ekZFyR2N4CvP9LetMUkZGBs6cOYPg4GD1OTMzMwQHB+O4aq+KV6Snp8PmlaVQtra2OPK/ApesrCwolcrXjgGA7OxsfPzxxxg3bhyqV6+uq7dEBXDlilRfYG0NNG8udzREZAhUBdxr1gDPnskaiuzWrAFSUoAqVaSNwanoyJokJSYmQqlUwt3dXeO8u7s74uPjtT4mJCQEs2fPxo0bN5CdnY2oqChs27YNDx48AAA4OjqiYcOGmDZtGu7fvw+lUol169bh+PHj6jEA8N1338HCwgL/+c9/ChRreno6kpOTNW70dlSX2t5/X+qRRET0Js2bAzVqSD2BVq6UOxr5CJHTYTsiQroMSUXH4D7euXPnolKlSvD394eVlRUiIiIwYMAAmOX6m7J27VoIIeDt7Q1ra2vMmzcPvXv3Vo85c+YM5s6di1WrVkFRwMYSM2bMgLOzs/pWlr3f3xq7bBNRYeVuB7BwobTnoynauxe4ehVwdATCwuSOxvjJmiS5urrC3NwcDx8+1Dj/8OFDeHh4aH2Mm5sbduzYgbS0NNy5cwdXr16Fg4MDKuSqXPPz88PBgweRmpqK2NhYnDx5EpmZmeoxhw8fRkJCAsqVKwcLCwtYWFjgzp07+Oyzz+Dr66v1dcePH4+kpCT1LTY2VjcfgolJTQUOHZKO2R+JiAqjTx+pJ9CtWzkz0qZGtcKvf38pUaKiJWuSZGVlhcDAQERHR6vPZWdnIzo6Gg0bNnztY21sbODt7Y2srCxs3boVnTp1yjPG3t4enp6eePr0KSIjI9VjPv74Y/zzzz84d+6c+ubl5YVx48YhMjJS6+tZW1vDyclJ40aFd+AAkJEB+PoClSvLHQ0RGRJ7e2DgQOlYdcnJlNy+DezaJR2Hh8sbi6mwkDuAMWPGICwsDHXr1kX9+vUxZ84cpKWlYcCAAQCAfv36wdvbGzNmzAAAnDhxAnFxcahduzbi4uIwefJkZGdn4/PPP1c/Z2RkJIQQqFKlCm7evIlx48bB399f/ZylSpVCqVKlNOKwtLSEh4cHqlSpUkzv3DSpfvtr25Yt9Imo8MLDgR9/lC7bX79uWr9sLV4s1SS1bi0VbVPRK/RMkq+vL6ZOnYq7d+/qJIDQ0FDMmjUL33zzDWrXro1z585hz5496mLuu3fvahRcv3z5EhMnTkS1atXQpUsXeHt748iRIyhRooR6TFJSEsLDw+Hv749+/fqhSZMmiIyMhKWlpU5iprcjhGaSRERUWBUqAO3bS8eqXkGm4PnznP3rVLVZVPQUQghRmAfMmTMHq1atwsWLF9GiRQsMGjQIXbp0gbW1dVHFqJeSk5Ph7OyMpKQkXnoroOvXpd9+rKyAx48BBwe5IyIiQ/Tnn0BIiFSTExdnGrU5y5YBQ4dKSeL164C5udwRGa7CfH8XeiZp9OjROHfuHE6ePImqVati1KhR8PT0REREBM6ePfvWQZPxU61qa9qUCRIRvb3gYOkyW0qK1DPI2AmRU7AdHs4EqTi9deF2nTp1MG/ePNy/fx+TJk3Czz//jHr16qF27dpYsWIFCjlBRSaAW5EQkS6YmUk9ggCpgNvYv24OHwYuXJD6yv2vtJaKyVsnSZmZmfjll1/w4Ycf4rPPPkPdunXx888/o1u3bvjqq6/Qp08fXcZJBu7FC2llG8B6JCJ6d2Fh0oz01atArgXSRkk1i9S3L+DiIm8spqbQq9vOnj2LlStXYuPGjTAzM0O/fv3w448/wt/fXz2mS5cuqFevnk4DJcN28CDw8iVQpgxQrZrc0RCRoXNyknoFLVggJRHGuj1HbCywfbt0rJo9o+JT6JmkevXq4caNG1i8eDHi4uIwa9YsjQQJAMqXL49evXrpLEgyfKp6JC79JyJdUfUK+v13ICZG3liKypIlgFIpbeNUs6bc0ZieQs8k3b59Gz4+Pq8dY29vj5WmvLkO5cF6JCLSNX9/oFUrICpK6iH0/fdyR6RbL18CS5dKx1z2L49CzyQlJCTgxIkTec6fOHECp0+f1klQZFxu35aWrFpYAC1byh0NERkTVfLw889SLyFj8ssvQGIiULYs8OGHckdjmgqdJIWHh2vdtywuLg7h7JNOWqgutTVqBDg7yxsLERmXdu2A8uWBp0+BDRvkjkZ3ci/7HzFC+iWTil+hk6TLly+jTp06ec6/9957uHz5sk6CIuOSux6JiEiXzM1zapPmzzeedgAnTgCnTwPW1sCQIXJHY7oKnSRZW1vj4cOHec4/ePAAFkx16RXp6cC+fdIx65GIqCgMHAjY2gL//CP1FDIGqlmk3r0BV1d5YzFlhU6SWrdujfHjxyMpKUl97tmzZ/jqq6/QqlUrnQZHhu/wYSAtDfDwAAIC5I6GiIyRi4vUQwiQWgIYuvh4YMsW6ZgF2/IqdJI0a9YsxMbGwsfHBy1atECLFi1Qvnx5xMfH44cffiiKGMmAqS61tWnDpf9EVHRUycS2bcC9e/LG8q6WLgUyM4GGDQEt1S1UjAqdJHl7e+Off/7B999/j2rVqiEwMBBz587FhQsXULZs2aKIkQyYauk/65GIqCjVrAk0by71FFqyRO5o3l5GRk78nEWSn0Jwk7W3UphdhE3V3buAj4+0z9KjR0DJknJHRETGbOtWoHt3wM1N+vfHxkbuiApv0yapDsnDA7hzB7Cykjsi41OY7++3rrS+fPky7t69i4yMDI3zH7KZA/2P6lJbgwZMkIio6HXqJG19dO+e1GOoXz+5Iyo8VcH28OFMkPTBW3Xc7tKlCy5cuACFQgHVRJTifwUnSqVStxGSwcpdj0REVNQsLKSeQhMmSAXchpYknT0LHDsmvY+hQ+WOhoC3qEn65JNPUL58eSQkJMDOzg6XLl3CoUOHULduXRxQbfNOJi8jA9i7VzpmPRIRFZchQ6TeQqdOSb2GDIlqZV6PHoCnp7yxkKTQSdLx48cxdepUuLq6wszMDGZmZmjSpAlmzJiB//znP0URIxmg48eBlBSpNoCrM4iouLi5Aar91VWXrgxBYmJOx3AWbOuPQidJSqUSjo6OAABXV1fcv38fAODj44Nr167pNjoyWKpVbSEhUuE2EVFxUSUZv/wi9RwyBD//LDXfDQyU6jhJPxT666tGjRo4f/48ACAoKAjff/89jh49iqlTp6JChQo6D5AME7ciISK5BAZKPYYyM6WeQ/ouKwtYtEg6HjWKPeX0SaGTpIkTJyI7OxsAMHXqVMTExKBp06b4f//v/2HevHk6D5AMz/37wPnz0v/orVvLHQ0RmaKICOnPJUukZEmf/f47EBsrbT8SGip3NJRboVe3hYSEqI8rVqyIq1ev4smTJ3BxcVGvcCPTFhkp/VmvHvccIiJ5dO8OfPYZ8OCB1IVbn5MPVe3UkCGG2dvJmBVqJikzMxMWFha4ePGixvmSJUsyQSI1VT0Sl/4TkVysrIBhw6RjfS7gvngR2L8fMDeX2heQfilUkmRpaYly5cqxFxLlKysLiIqSjlmPRERyGjZM6jl09Cjw999yR6Odatl/584Ad/bSP4WuSZowYQK++uorPHnypCjiIQN34gTw7JnUYbtePbmjISJT5ukp9RwCcpIRffL0KbB2rXTMZf/6qdA1SQsWLMDNmzfh5eUFHx8f2Nvba/z87NmzOguODI9qVVvr1tL0MRGRnCIigI0bpR5E338PlCold0Q5Vq0Cnj8HatQAmjWTOxrSptBJUufOnYsgDDIWrEciIn3SsKHU0PbsWakX0RdfyB2RJDsbWLhQOuayf/2lEKrN16hQCrOLsKl4+FDauRqQVpSojomI5LRqFTBgAFCuHHDrllSnJLc//gA6dABKlJA25H3logwVocJ8f7MXMunMn39Kf773HhMkItIfvXpJ7Uju3pV6EukD1Yq7QYOYIOmzQidJZmZmMDc3z/dGpkt1qY2r2ohIn9jYSD2IAP0o4L52Teonp1AAI0fKHQ29TqEnHbdv365xPzMzE3///TdWr16NKVOm6CwwMixKZc5MEuuRiEjfDB8OfPcdsG8fcOkSUL26fLGotiBp3x7gbl76TWc1SRs2bMDmzZvx22+/6eLp9B5rkjSdOCFtyujsLO1mrQ/X/ImIcuvWTeq+PXw4sHixPDGkpADe3tKfkZHcukkOstQkNWjQANHR0bp6OjIwqqX/wcFMkIhIP6l6Ea1ZI/Vzk8OaNVKCVKWK9O8l6TedJEkvXrzAvHnz4O3trYunIwPEeiQi0nfNm0s9iZ4/B1auLP7XFyKnJioiAjDj0im9V+jf+V/dyFYIgZSUFNjZ2WHdunU6DY4Mw+PHwMmT0nGu/Y+JiPSKQiElJ8OHSz2KPvmkeBOV6Gjg6lXA0REICyu+16W3V+gk6ccff9RIkszMzODm5oagoCC4uLjoNDgyDH/+Kf2GVLMmUKaM3NEQEeWvb1/gyy+lfkl79gDt2hXfa6uW/YeFSYkS6b9CJ0n9+/cvgjDIkKnqkbiqjYj0nb09MHAgMHu2lLQUV5IUE5PToykionhek95doScaV65ciS1btuQ5v2XLFqxevVonQZHhyM7OSZJYj0REhiA8XLr0tmcPcP168bzmokXSjHvr1lLRNhmGQidJM2bMgKura57zpUuXxn//+1+dBEWG49w5ICEBcHAAGjeWOxoiojerUEHqUQTk7J9WlJ4/B5Yvl45VK+zIMBQ6Sbp79y7Kly+f57yPjw/u3r2rk6DIcKhWtbVsCVhZyRsLEVFBqS55rVolLckvShs2AE+fSskZZ9wNS6GTpNKlS+Off/7Jc/78+fMoVaqUToIiw8FLbURkiFq1AipXBpKTgbVri+51hMgp2B45EuDuXYal0ElS79698Z///Af79++HUqmEUqnEvn378Mknn6BXr15FESPpqWfPgOPHpWMWbRORITEzy5lNWrBASmaKwuHDwD//AHZ2UsE4GZZCJ0nTpk1DUFAQWrZsCVtbW9ja2qJ169b44IMPWJNkYvbulfZsq1oV8PGROxoiosIJC5PqKa9ckXoYFQXVLFLfvgC75BieQidJVlZW2Lx5M65du4b169dj27ZtuHXrFlasWAErFqWYFFU9EmeRiMgQOTkBqq42qmRGl2JjAdWe8Fz2b5jeepetSpUqoVKlSrqMhQyIEKxHIiLDFx4uXW77/Xfg338BX1/dPfdPP0mz7c2bS812yfAUeiapW7du+O677/Kc//7779GjRw+dBEX678IF4P596Tp706ZyR0NE9Hb8/aUibiGkXka68vIlsHSpdMxl/4ar0EnSoUOH0E5Li9K2bdvi0KFDOgmK9J/qUluLFoCNjbyxEBG9C1US8/PPUk8jXfjlF+DRI6BsWaBTJ908JxW/QidJqampWmuPLC0tkZycrJOgSP9xKxIiMhbt2gHly0u9jDZsePfny73sf8QIwOKtC1tIboVOkmrWrInNmzfnOb9p0yZUq1ZNJ0GRfktOBo4ckY5Zj0REhs7cXKpNAnTTDuDECeD0acDaGhg8+N3jI/kUOr/9+uuv0bVrV9y6dQsffPABACA6OhobNmzAr7/+qvMASf/s2wdkZQEVKwJ+fnJHQ0T07gYMAL7+Gjh/Xvol8F1qLRcskP7s1Qtwc9NNfCSPQs8kdezYETt27MDNmzcxcuRIfPbZZ4iLi8O+fftQsWLFooiR9IyqHomzSERkLEqWlHoZAe/WDiA+XqpHAliwbQwKnSQBQPv27XH06FGkpaXh9u3b6NmzJ8aOHYuAgABdx0d6JvfSf9YjEZExUSU127YB9+693XMsXQpkZgINGwKBgbqLjeTxVkkSIK1yCwsLg5eXF3744Qd88MEH+Ouvv97quRYuXAhfX1/Y2NggKCgIJ0+ezHdsZmYmpk6dCj8/P9jY2CAgIAB7VN/a/5OSkoLRo0fDx8cHtra2aNSoEU6dOqXxHF988QVq1qwJe3t7eHl5oV+/frh///5bxW9KrlwB7t6VrrW//77c0RAR6U7NmlJPI6USWLKk8I/PyMh5HGeRjEOhkqT4+HjMnDkTlSpVQo8ePeDk5IT09HTs2LEDM2fORL169QodwObNmzFmzBhMmjQJZ8+eRUBAAEJCQpCQkKB1/MSJE/HTTz9h/vz5uHz5MoYPH44uXbrg77//Vo8ZPHgwoqKisHbtWly4cAGtW7dGcHAw4uLiAADPnz/H2bNn8fXXX+Ps2bPYtm0brl27hg8//LDQ8ZsaVT7avLnUI4mIyJiokpulS4H09MI9dvt24MEDwMMD6NZN97GRDEQBdejQQTg5OYnevXuLXbt2iaysLCGEEBYWFuLSpUsFfZo86tevL8LDw9X3lUql8PLyEjNmzNA63tPTUyxYsEDjXNeuXUWfPn2EEEI8f/5cmJubi127dmmMqVOnjpgwYUK+cZw8eVIAEHfu3ClQ3ElJSQKASEpKKtB4YxEcLAQgxI8/yh0JEZHuZWYKUaaM9O/cmjWFe2zjxtLjJk0qktBIRwrz/V3gmaTdu3dj0KBBmDJlCtq3bw9zc/N3TtAyMjJw5swZBAcHq8+ZmZkhODgYx1Xby78iPT0dNq90L7S1tcWR/61Jz8rKglKpfO0YbZKSkqBQKFCiRIl8Xzc5OVnjZmrS0gBVv1DWIxGRMbKwkHobAYUr4P77b+DoUenxw4YVTWxU/AqcJB05cgQpKSkIDAxEUFAQFixYgMTExHd68cTERCiVSri7u2ucd3d3R3x8vNbHhISEYPbs2bhx4ways7MRFRWFbdu24cGDBwAAR0dHNGzYENOmTcP9+/ehVCqxbt06HD9+XD3mVS9fvsQXX3yB3r17w8nJSeuYGTNmwNnZWX0rW7bsO7xzw7R/v3TN3dcXqFJF7miIiIrGkCFS3eWpU1LPo4JQJVQ9egCenkUXGxWvAidJDRo0wLJly/DgwQMMGzYMmzZtgpeXlzpRSUlJKco41ebOnYtKlSrB398fVlZWiIiIwIABA2BmlvNW1q5dCyEEvL29YW1tjXnz5qF3794aY1QyMzPRs2dPCCGwePHifF93/PjxSEpKUt9iY2OL5P3ps9yr2hQKeWMhIioqbm5SjyOgYLNJiYk5nbpZsG1cCr26zd7eHgMHDsSRI0dw4cIFfPbZZ5g5cyZKly5d6MJnV1dXmJub4+HDhxrnHz58CA8PD62PcXNzw44dO5CWloY7d+7g6tWrcHBwQIUKFdRj/Pz8cPDgQaSmpiI2NhYnT55EZmamxhggJ0G6c+cOoqKi8p1FAgBra2s4OTlp3EyJEOyPRESmQ5Xs/PIL8MpXVB7Ll0tF3oGBQIMGRR8bFZ+3bgEAAFWqVMH333+Pe/fuYePGjYV+vJWVFQIDAxEdHa0+l52djejoaDRs2PC1j7WxsYG3tzeysrKwdetWdNKyg6C9vT08PT3x9OlTREZGaoxRJUg3btzA3r17UapUqULHb0pu3gRu3wYsLYH/NVonIjJaqoQnM1Na6ZafrCxg0SLpOCKCs+zG5p2SJBVzc3N07twZO3fuLPRjx4wZg2XLlmH16tW4cuUKRowYgbS0NAwYMAAA0K9fP4wfP149/sSJE9i2bRtu376Nw4cPo02bNsjOzsbnn3+uHhMZGYk9e/YgJiYGUVFRaNGiBfz9/dXPmZmZie7du+P06dNYv349lEol4uPjER8fj4yMjHf8NIyTahapaVPAwUHeWIiIioNqNmnJEilZ0ub336Xeca6uOZfoyHjIvjdxaGgoHj16hG+++Qbx8fGoXbs29uzZoy7mvnv3rkYt0cuXLzFx4kTcvn0bDg4OaNeuHdauXauxKi0pKQnjx4/HvXv3ULJkSXTr1g3Tp0+HpaUlACAuLk6d0NWuXVsjnv379+N9dknMg5faiMjUdO8OfPYZcP++1IU7NDTvGFXN0pAhwCuLqskIKIR41/2OTVNycjKcnZ2RlJRk9PVJL15I+xq9fAlcuADUqCF3RERExWPyZGDKFKBxY2nj29wuXpS6dJubAzExgAkuejZIhfn+1snlNjJuBw9KCVKZMkD16nJHQ0RUfIYNk3ofHT0q9ULKbeFC6c/OnZkgGSsmSfRGXPpPRKbK01O67AYACxbknH/2DFizRjqOiCj2sKiYMEmiN2I9EhGZMlUB94YNwOPH0vHKlcDz51L5QfPm8sVGRYtJEr3W7dvA9evSdHPLlnJHQ0RU/Bo2BOrUkcoOfv4ZyM7OudQ2ahRn2I0ZkyR6LdWltkaNAGdneWMhIpKDQpEzm7RoEbBrF3DrFlCiBNCnj6yhURFjkkSvlbseiYjIVIWGAqVKST2RhgyRzg0cCNjbyxsXFS0mSZSv9HRg3z7pmPVIRGTKbG1zkqOEBGl2KTxc3pio6DFJonwdOQKkpQEeHkBAgNzREBHJa8QIQNXbuH174JXtQMkIMUmifKlWtXHpPxERUK4c0L+/tJDliy/kjoaKA5MkyhfrkYiINP30ExAfDzRpInckVByYJJFWsbHApUvS1HKrVnJHQ0SkHywspAJuMg1Mkkgr1SxSUJC0bxsREZGpYZJEWrHLNhERmTomSZRHRgawd690zHokIiIyVUySKI/jx4GUFMDNDQgMlDsaIiIieTBJojxUl9pCQnJ6ghAREZkafgVSHlz6T0RExCSJXnH/PnD+vNQ8snVruaMhIiKSD5Mk0hAZKf1Zt65Uk0RERGSqmCSRBi79JyIikjBJIrWsLCAqSjpmPRIREZk6JkmkduIE8OwZ4OIC1K8vdzRERETyYpJEaqpVba1bA+bm8sZCREQkNyZJpMZ6JCIiohxMkggAkJAAnDkjHYeEyBsLERGRPmCSRABylv6/9x7g4SFvLERERPqASRIBYJdtIiKiVzFJIiiVOTNJrEciIiKSMEkinDkDPH4MODkBDRrIHQ0REZF+YJJE6lVtrVoBlpbyxkJERKQvmCSROkliPRIREVEOJkkm7vFj4ORJ6ZhJEhERUQ4mSSbuzz8BIYAaNYAyZeSOhoiISH8wSTJxqqX/XNVGRESkiUmSCcvOZn8kIiKi/DBJMmHnzknbkTg4AE2ayB0NERGRfmGSZMJUq9patgSsrOSNhYiISN8wSTJhvNRGRESUPyZJJurZM+D4cemYSRIREVFeTJJM1N690p5t/v6Ar6/c0RAREekfJkkmSlWPxKX/RERE2jFJMkFCsB6JiIjoTZgkmaALF4D79wFbW6BZM7mjISIi0k9MkkyQahapRQvAxkbeWIiIiPQVkyQTxHokIiKiN2OSZGJSUoAjR6Rj1iMRERHlj0mSiYmOBrKygIoVpRsRERFpxyTJxKgutXEWiYiI6PWYJJmQ3Ev/WY9ERET0ekySTMiVK8Ddu4C1NfD++3JHQ0REpN+YJJkQ1SxS8+aAnZ28sRAREek7JkkmhPVIREREBacXSdLChQvh6+sLGxsbBAUF4eTJk/mOzczMxNSpU+Hn5wcbGxsEBARgj2qK5H9SUlIwevRo+Pj4wNbWFo0aNcKpU6c0xggh8M0338DT0xO2trYIDg7GjRs3iuT96YO0NODQIemY9UhERERvJnuStHnzZowZMwaTJk3C2bNnERAQgJCQECQkJGgdP3HiRPz000+YP38+Ll++jOHDh6NLly74+++/1WMGDx6MqKgorF27FhcuXEDr1q0RHByMuLg49Zjvv/8e8+bNw5IlS3DixAnY29sjJCQEL1++LPL3LIf9+4GMDMDHB6hSRe5oiIiIDICQWf369UV4eLj6vlKpFF5eXmLGjBlax3t6eooFCxZonOvatavo06ePEEKI58+fC3Nzc7Fr1y6NMXXq1BETJkwQQgiRnZ0tPDw8xP/93/+pf/7s2TNhbW0tNm7cWKC4k5KSBACRlJRUoPFyCw8XAhBi+HC5IyEiIpJPYb6/ZZ1JysjIwJkzZxAcHKw+Z2ZmhuDgYBw/flzrY9LT02HzyoZjtra2OPK/NtJZWVlQKpWvHRMTE4P4+HiN13V2dkZQUNBrXzc5OVnjZiiE4FYkREREhSVrkpSYmAilUgl3d3eN8+7u7oiPj9f6mJCQEMyePRs3btxAdnY2oqKisG3bNjx48AAA4OjoiIYNG2LatGm4f/8+lEol1q1bh+PHj6vHqJ67MK87Y8YMODs7q29ly5Z9p/denG7eBG7fBiwtpU1tiYiI6M1kr0kqrLlz56JSpUrw9/eHlZUVIiIiMGDAAJiZ5byVtWvXQggBb29vWFtbY968eejdu7fGmMIaP348kpKS1LfY2FhdvJ1ioZpFatoUcHSUNxYiIiJDIWuS5OrqCnNzczx8+FDj/MOHD+Hh4aH1MW5ubtixYwfS0tJw584dXL16FQ4ODqhQoYJ6jJ+fHw4ePIjU1FTExsbi5MmTyMzMVI9RPXdhXtfa2hpOTk4aN0OhWvzHpf9EREQFJ2uSZGVlhcDAQERHR6vPZWdnIzo6Gg0bNnztY21sbODt7Y2srCxs3boVnTp1yjPG3t4enp6eePr0KSIjI9VjypcvDw8PD43XTU5OxokTJ974uobmxQtpZRvAeiQiIqLCsJA7gDFjxiAsLAx169ZF/fr1MWfOHKSlpWHAgAEAgH79+sHb2xszZswAAJw4cQJxcXGoXbs24uLiMHnyZGRnZ+Pzzz9XP2dkZCSEEKhSpQpu3ryJcePGwd/fX/2cCoUCo0ePxrfffotKlSqhfPny+Prrr+Hl5YXOnTsX+2dQlA4dAl6+BLy9gerV5Y6GiIjIcMieJIWGhuLRo0f45ptvEB8fj9q1a2PPnj3qouq7d+9q1BK9fPkSEydOxO3bt+Hg4IB27dph7dq1KFGihHpMUlISxo8fj3v37qFkyZLo1q0bpk+fDktLS/WYzz//HGlpaRg6dCiePXuGJk2aYM+ePXlWxRm63KvaFAp5YyEiIjIkCiGEkDsIQ5ScnAxnZ2ckJSXpdX2Svz9w7Rrw669At25yR0NERCSvwnx/G9zqNiq4mBgpQTI3B3K1hCIiIqICYJJkxFSX2ho1Apyd5Y2FiIjI0DBJMmKqpf9c1UZERFR4TJKMVHo6sG+fdMz+SERERIXHJMlIHTkCpKUBHh5A7dpyR0NERGR4mCQZKVU9UkgIl/4TERG9DSZJRor1SERERO+GSZIRio0FLl0CzMyAVq3kjoaIiMgwMUkyQqpZpKAgoGRJeWMhIiIyVEySjJCqHomr2oiIiN4ekyQjk5kJ7N0rHbMeiYiI6O0xSTIyx44BKSmAqysQGCh3NERERIaLSZKRUdUjhYRIhdtERET0dvg1amRU9Ui81EZERPRumCQZkfv3gfPnpeaRrVvLHQ0REZFhY5JkRCIjpT/r1gXc3OSNhYiIyNAxSTIiqnokLv0nIiJ6d0ySjERWFvDnn9Ix65GIiIjeHZMkI3HiBPDsGeDiAtSvL3c0REREho9JkpFQXWpr3RowN5c3FiIiImPAJMlIcCsSIiIi3WKSZAQSEoAzZ6TjkBB5YyEiIjIWTJKMgGrpf+3agKenrKEQEREZDSZJRkBVj8RVbURERLrDJMnAKZU5M0msRyIiItIdJkkG7swZ4PFjwMkJaNhQ7miIiIiMB5MkA6da1RYcDFhayhsLERGRMWGSZOBYj0RERFQ0mCQZsMePpU7bAOuRiIiIdI1JkgGLigKEAGrUAMqUkTsaIiIi48IkyYCxyzYREVHRYZJkoLKzc5b+sx6JiIhI9yzkDoDezrlzwMOHgL090KSJ3NEQkaFRKpXIzMyUOwwinbO0tIS5jnZ6Z5JkoFSr2lq2BKys5I2FiAyHEALx8fF49uyZ3KEQFZkSJUrAw8MDCoXinZ6HSZKBUtUj8VIbERWGKkEqXbo07Ozs3vlLhEifCCHw/PlzJCQkAAA833FDUyZJBujZM+D4cemYRdtEVFBKpVKdIJUqVUrucIiKhK2tLQAgISEBpUuXfqdLbyzcNkB790p7tvn7A76+ckdDRIZCVYNkZ2cncyRERUv1d/xd6+6YJBkgLv0nonfBS2xk7HT1d5xJkoERgluREBHpgq+vL+bMmSN3GKTHmCQZmAsXgPv3AVtboFkzuaMhIip6CoXitbfJkye/1fOeOnUKQ4cO1UmMGzduhLm5OcLDw3XyfKQfmCQZGNUsUosWgI2NvLEQERWHBw8eqG9z5syBk5OTxrmxY8eqxwohkJWVVaDndXNz01l91vLly/H5559j48aNePnypU6e821lZGTI+vrGhEmSgWE9EhGZGg8PD/XN2dkZCoVCff/q1atwdHTE7t27ERgYCGtraxw5cgS3bt1Cp06d4O7uDgcHB9SrVw979+7VeN5XL7cpFAr8/PPP6NKlC+zs7FCpUiXs3LnzjfHFxMTg2LFj+PLLL1G5cmVs27Ytz5gVK1agevXqsLa2hqenJyIiItQ/e/bsGYYNGwZ3d3fY2NigRo0a2LVrFwBg8uTJqF27tsZzzZkzB765Vu30798fnTt3xvTp0+Hl5YUqVaoAANauXYu6devC0dERHh4e+Oijj9RL41UuXbqEDh06wMnJCY6OjmjatClu3bqFQ4cOwdLSEvHx8RrjR48ejaZNm77xMzEWTJIMSEoKcOSIdMx6JCLSBSGAtDR5bkLo7n18+eWXmDlzJq5cuYJatWohNTUV7dq1Q3R0NP7++2+0adMGHTt2xN27d1/7PFOmTEHPnj3xzz//oF27dujTpw+ePHny2sesXLkS7du3h7OzM/r27Yvly5dr/Hzx4sUIDw/H0KFDceHCBezcuRMVK1YEAGRnZ6Nt27Y4evQo1q1bh8uXL2PmzJmFXrYeHR2Na9euISoqSp1gZWZmYtq0aTh//jx27NiBf//9F/3791c/Ji4uDs2aNYO1tTX27duHM2fOYODAgcjKykKzZs1QoUIFrF27Vj0+MzMT69evx8CBAwsVm0ET9FaSkpIEAJGUlFRsr7l9uxCAEH5+xfaSRGREXrx4IS5fvixevHihPpeaKv27IsctNbXw72HlypXC2dlZfX///v0CgNixY8cbH1u9enUxf/589X0fHx/x448/qu8DEBMnTsz12aQKAGL37t35PqdSqRRly5ZVv/6jR4+ElZWVuH37tnqMl5eXmDBhgtbHR0ZGCjMzM3Ht2jWtP580aZIICAjQOPfjjz8KHx8f9f2wsDDh7u4u0tPT841TCCFOnTolAIiUlBQhhBDjx48X5cuXFxkZGVrHf/fdd6Jq1arq+1u3bhUODg4i9W3+wxUzbX/XVQrz/c2ZJAPCVW1ERNrVrVtX435qairGjh2LqlWrokSJEnBwcMCVK1feOJNUq1Yt9bG9vT2cnJzyXKLKLSoqCmlpaWjXrh0AwNXVFa1atcKKFSsASA0N79+/j5YtW2p9/Llz51CmTBlUrly5QO8zPzVr1oTVK3tUnTlzBh07dkS5cuXg6OiI5s2bA4D6Mzh37hyaNm0KS0tLrc/Zv39/3Lx5E3/99RcAYNWqVejZsyfs7e3fKVZDwo7bBkII1iMRke7Z2QGpqfK9tq68+sU9duxYREVFYdasWahYsSJsbW3RvXv3NxY1v5owKBQKZGdn5zt++fLlePLkibrLMyBdQvvnn38wZcoUjfPavOnnZmZmEK9cl9TWIPHV95+WloaQkBCEhIRg/fr1cHNzw927dxESEqL+DN702qVLl0bHjh2xcuVKlC9fHrt378aBAwde+xhjwyTJQFy9Cty9C1hbA++/L3c0RGQsFArAGCcGjh49iv79+6NLly4ApJmlf//9V6ev8fjxY/z222/YtGkTqlevrj6vVCrRpEkT/Pnnn2jTpg18fX0RHR2NFi1a5HmOWrVq4d69e7h+/brW2SQ3NzfEx8dDCKFukHju3Lk3xnb16lU8fvwYM2fORNmyZQEAp0+fzvPaq1evRmZmZr6zSYMHD0bv3r1RpkwZ+Pn5oXHjxm98bWPCy20GQjWL1KyZcf6DRkSkS5UqVcK2bdtw7tw5nD9/Hh999NFrZ4Textq1a1GqVCn07NkTNWrUUN8CAgLQrl07dQH35MmT8cMPP2DevHm4ceMGzp49i/nz5wMAmjdvjmbNmqFbt26IiopCTEwMdu/ejT3/q694//338ejRI3z//fe4desWFi5ciN2qL4TXKFeuHKysrDB//nzcvn0bO3fuxLRp0zTGREREIDk5Gb169cLp06dx48YNrF27FteuXVOPCQkJgZOTE7799lsMGDBAVx+dwWCSZCBYj0REVHCzZ8+Gi4sLGjVqhI4dOyIkJAR16tTR6WusWLECXbp00boFRrdu3bBz504kJiYiLCwMc+bMwaJFi1C9enV06NABN27cUI/dunUr6tWrh969e6NatWr4/PPPoVQqAQBVq1bFokWLsHDhQgQEBODkyZMafaHy4+bmhlWrVmHLli2oVq0aZs6ciVmzZmmMKVWqFPbt24fU1FQ0b94cgYGBWLZsmcaskpmZGfr37w+lUol+/fq97UdlsBTi1YudVCDJyclwdnZGUlISnJycivS10tKAkiWBjAzg8mWgatUifTkiMlIvX75ETEwMypcvDxt2o6UCGjRoEB49elSgnlH64nV/1wvz/c2aJANw4ICUIPn4AP7+ckdDRESmICkpCRcuXMCGDRsMKkHSJdkvty1cuBC+vr6wsbFBUFAQTp48me/YzMxMTJ06FX5+frCxsUFAQID6uq2KUqnE119/jfLly8PW1hZ+fn6YNm2axuqA1NRUREREoEyZMrC1tUW1atWwZMmSInuP70p1+bltW6nIkoiIqKh16tQJrVu3xvDhw9GqVSu5w5GFrDNJmzdvxpgxY7BkyRIEBQVhzpw5CAkJwbVr11C6dOk84ydOnIh169Zh2bJl8Pf3R2RkJLp06YJjx47hvffeAwB89913WLx4MVavXo3q1avj9OnTGDBgAJydnfGf//wHADBmzBjs27cP69atg6+vL/7880+MHDkSXl5e+PDDD4v1M3gTLv0nIiI5mNpyf21krUkKCgpCvXr1sGDBAgBSb4myZcti1KhR+PLLL/OM9/LywoQJEzR2We7WrRtsbW2xbt06AECHDh3g7u6u0Rb+1TE1atRAaGgovv76a/WYwMBAtG3bFt9++22BYi+umqQbN4DKlQFLS+DxY8DRscheioiMHGuSyFToqiZJtsttGRkZOHPmDIKDg3OCMTNDcHAwjh8/rvUx6enped6sra0tjqg2NAPQqFEjREdH4/r16wCA8+fP48iRI2iba1lYo0aNsHPnTsTFxUEIgf379+P69eto3bq1Lt+iTqhmkZo0YYJERERUnGS73JaYmAilUgl3d3eN8+7u7rh69arWx4SEhGD27Nlo1qwZ/Pz8EB0djW3btqmXSgLSJofJycnw9/eHubk5lEolpk+fjj59+qjHzJ8/H0OHDkWZMmVgYWEBMzMzLFu2DM2aNcs33vT0dKSnp6vvJycnv+1bLxQu/SciIpKH7IXbhTF37lxUqlQJ/v7+sLKyQkREBAYMGAAzs5y38csvv2D9+vXYsGEDzp49i9WrV2PWrFlYvXq1esz8+fPx119/YefOnThz5gx++OEHhIeHY+/evfm+9owZM+Ds7Ky+qTqYFqUXL4D9+6Vj1iMREREVL9lmklxdXWFubo6HDx9qnH/48CE8PDy0PsbNzQ07duzAy5cv8fjxY3h5eeHLL79EhQoV1GPGjRuHL7/8Er169QIgbfp3584dzJgxA2FhYXjx4gW++uorbN++He3btwcgtWY/d+4cZs2apXH5L7fx48djzJgx6vvJyclFnigdOgS8fAl4ewM1ahTpSxEREdErZJtJsrKyQmBgIKKjo9XnsrOzER0djYYNG772sTY2NvD29kZWVha2bt2KTp06qX/2/PlzjZklADA3N1e3o8/MzERmZuZr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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "67" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#----------Инициализируем модель и параметры обучения--------------\n", + "\n", + "torch.cuda.empty_cache()\n", + "cv2.destroyAllWindows()\n", + "gc.collect()\n", + "\n", + "config_name = \"ensemble\"\n", + " \n", + "def load_function(attr):\n", + " module_, func = attr.rsplit('.', maxsplit=1)\n", + " return getattr(import_module(module_), func)\n", + " \n", + "config = mlconfig.load('config_' + config_name + '.yaml')\n", + "\n", + "model1 = models.resnet18(pretrained=False)\n", + "model2 = models.resnet50(pretrained=False)\n", + "\n", + "num_classes = 2\n", + "\n", + "model1.fc = nn.Linear(model1.fc.in_features, num_classes)\n", + "model2.fc = nn.Linear(model2.fc.in_features, num_classes)\n", + "\n", + "class Ensemble(nn.Module):\n", + " def __init__(self, model1, model2):\n", + " super(Ensemble, self).__init__()\n", + " self.model1 = model1\n", + " self.model2 = model2\n", + " self.fc = nn.Linear(2 * num_classes, num_classes)\n", + "\n", + " def forward(self, x):\n", + " x1 = self.model1(x[0])\n", + " x2 = self.model2(x[1])\n", + " x = torch.cat((x1, x2), dim=1)\n", + " x = self.fc(x)\n", + " return x\n", + "model = Ensemble(model1, model2)\n", + "\n", + "optimizer = load_function(config.optimizer.name)(model.parameters(), lr=config.optimizer.lr)\n", + "criterion = load_function(config.loss_function.name)()\n", + "scheduler = load_function(config.scheduler.name)(optimizer, step_size=config.scheduler.step_size, gamma=config.scheduler.gamma)\n", + "\n", + "if device != 'cpu':\n", + " model = model.to(device)\n", + "\n", + "#----------Создания датасета и обучение модели--------------\n", + "\n", + "path_res, model_name = prepare_and_learning_detection(num_classes = num_classes, num_samples = 10000, path_dataset = \"/mnt/nvme1/dataset_img\", \n", + " selected_freq=2400,model_name = config_name+\"2400_\", config_name = config_name, model=model)\n", + "\n", + "\n", + "torch.cuda.empty_cache()\n", + "cv2.destroyAllWindows()\n", + "del model\n", + "gc.collect()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4234ee26", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Отсутствует", + "kernelspec": { + "display_name": ".venv-train", + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/train_scripts/Training_models_1.2.ipynb b/train_scripts/Training_models 3pic.ipynb similarity index 100% rename from train_scripts/Training_models_1.2.ipynb rename to train_scripts/Training_models 3pic.ipynb From 42c724f227590cdbc42a9d5e4e86c71f2258dbf0 Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Thu, 9 Apr 2026 11:20:11 +0700 Subject: [PATCH 4/8] =?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20inference=20=D0=BD=D0=B0=20=D0=B4=D0=B2?= =?UTF-8?q?=D1=83=D1=85=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA=D0=B0?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NN_server/Models/ensemble_1200_v44.py | 197 ++++++++++++++++++++++++++ NN_server/Models/ensemble_2400_v44.py | 197 ++++++++++++++++++++++++++ NN_server/Models/ensemble_915_v44.py | 197 ++++++++++++++++++++++++++ 3 files changed, 591 insertions(+) create mode 100644 NN_server/Models/ensemble_1200_v44.py create mode 100644 NN_server/Models/ensemble_2400_v44.py create mode 100644 NN_server/Models/ensemble_915_v44.py diff --git a/NN_server/Models/ensemble_1200_v44.py b/NN_server/Models/ensemble_1200_v44.py new file mode 100644 index 0000000..d805a4f --- /dev/null +++ b/NN_server/Models/ensemble_1200_v44.py @@ -0,0 +1,197 @@ +from torchvision import models +import torch.nn as nn +import matplotlib +import numpy as np +import torch +import cv2 +import gc +import io + + +def _render_plot(values, figsize=(16, 16), dpi=16): + import matplotlib.pyplot as plt + + fig = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + plt.plot(values, color="black") + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + + buf = io.BytesIO() + fig.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + + img = cv2.imdecode(img_arr, 1) + if img is None: + raise RuntimeError("failed to decode plot image") + + plt.clf() + plt.cla() + plt.close() + plt.close(fig) + + return np.asarray(cv2.split(img), dtype=np.float32) + + +def pre_func_ensemble(data=None, src="", ind_inference=0): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + + real = np.asarray(data[0], dtype=np.float32) + imag = np.asarray(data[1], dtype=np.float32) + signal = real + 1j * imag + + img_real = _render_plot(signal.real) + img_mag = _render_plot(np.abs(signal)) + + cv2.destroyAllWindows() + gc.collect() + + print("Подготовка данных завершена") + print() + return [img_real, img_mag] + + except Exception as exc: + print(str(exc)) + return None + + +def build_func_ensemble(file_model="", file_config="", num_classes=None): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + torch.cuda.empty_cache() + + num_classes = 2 + model1 = models.resnet18(pretrained=False) + model2 = models.resnet50(pretrained=False) + + model1.fc = nn.Linear(model1.fc.in_features, num_classes) + model2.fc = nn.Linear(model2.fc.in_features, num_classes) + + class Ensemble(nn.Module): + def __init__(self, model1, model2): + super().__init__() + self.model1 = model1 + self.model2 = model2 + self.fc = nn.Linear(2 * num_classes, num_classes) + + def forward(self, x): + if isinstance(x, (list, tuple)): + x1 = x[0] + x2 = x[1] if len(x) > 1 else x[0] + else: + x1 = x + x2 = x + y1 = self.model1(x1) + y2 = self.model2(x2) + y = torch.cat((y1, y2), dim=1) + return self.fc(y) + + model = Ensemble(model1, model2) + + device = "cuda" if torch.cuda.is_available() else "cpu" + if device != "cpu": + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print("Инициализация модели завершена") + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_ensemble(data=None, model=None, mapping=None, shablon=""): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + + device = "cuda" if torch.cuda.is_available() else "cpu" + if isinstance(data, (list, tuple)) and len(data) >= 2: + inputs = [ + torch.unsqueeze(torch.tensor(data[0]).cpu(), 0).to(device).float(), + torch.unsqueeze(torch.tensor(data[1]).cpu(), 0).to(device).float(), + ] + else: + tensor = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device).float() + inputs = [tensor, tensor] + + with torch.no_grad(): + output = model(inputs) + _, predict = torch.max(output.data, 1) + + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print("PREDICTION" + shablon + ": " + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + cv2.destroyAllWindows() + gc.collect() + print("Уверенность" + shablon + " в предсказании: " + str(probability)) + print("Инференс завершен") + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_ensemble(src="", model_type="", prediction="", model_id=0, ind_inference=0, data=None): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + + if int(ind_inference) <= 100 and isinstance(data, (list, tuple)) and len(data) >= 2: + fig, ax = plt.subplots() + ax.imshow(np.moveaxis(data[0], 0, -1)) + plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_real_" + str(model_id) + "_" + model_type + ".png") + plt.clf() + plt.cla() + plt.close(fig) + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(np.moveaxis(data[1], 0, -1)) + plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_mod_" + str(model_id) + "_" + model_type + ".png") + plt.clf() + plt.cla() + plt.close(fig) + cv2.destroyAllWindows() + gc.collect() + + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print("Постобработка завершена") + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/ensemble_2400_v44.py b/NN_server/Models/ensemble_2400_v44.py new file mode 100644 index 0000000..d805a4f --- /dev/null +++ b/NN_server/Models/ensemble_2400_v44.py @@ -0,0 +1,197 @@ +from torchvision import models +import torch.nn as nn +import matplotlib +import numpy as np +import torch +import cv2 +import gc +import io + + +def _render_plot(values, figsize=(16, 16), dpi=16): + import matplotlib.pyplot as plt + + fig = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + plt.plot(values, color="black") + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + + buf = io.BytesIO() + fig.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + + img = cv2.imdecode(img_arr, 1) + if img is None: + raise RuntimeError("failed to decode plot image") + + plt.clf() + plt.cla() + plt.close() + plt.close(fig) + + return np.asarray(cv2.split(img), dtype=np.float32) + + +def pre_func_ensemble(data=None, src="", ind_inference=0): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + + real = np.asarray(data[0], dtype=np.float32) + imag = np.asarray(data[1], dtype=np.float32) + signal = real + 1j * imag + + img_real = _render_plot(signal.real) + img_mag = _render_plot(np.abs(signal)) + + cv2.destroyAllWindows() + gc.collect() + + print("Подготовка данных завершена") + print() + return [img_real, img_mag] + + except Exception as exc: + print(str(exc)) + return None + + +def build_func_ensemble(file_model="", file_config="", num_classes=None): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + torch.cuda.empty_cache() + + num_classes = 2 + model1 = models.resnet18(pretrained=False) + model2 = models.resnet50(pretrained=False) + + model1.fc = nn.Linear(model1.fc.in_features, num_classes) + model2.fc = nn.Linear(model2.fc.in_features, num_classes) + + class Ensemble(nn.Module): + def __init__(self, model1, model2): + super().__init__() + self.model1 = model1 + self.model2 = model2 + self.fc = nn.Linear(2 * num_classes, num_classes) + + def forward(self, x): + if isinstance(x, (list, tuple)): + x1 = x[0] + x2 = x[1] if len(x) > 1 else x[0] + else: + x1 = x + x2 = x + y1 = self.model1(x1) + y2 = self.model2(x2) + y = torch.cat((y1, y2), dim=1) + return self.fc(y) + + model = Ensemble(model1, model2) + + device = "cuda" if torch.cuda.is_available() else "cpu" + if device != "cpu": + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print("Инициализация модели завершена") + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_ensemble(data=None, model=None, mapping=None, shablon=""): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + + device = "cuda" if torch.cuda.is_available() else "cpu" + if isinstance(data, (list, tuple)) and len(data) >= 2: + inputs = [ + torch.unsqueeze(torch.tensor(data[0]).cpu(), 0).to(device).float(), + torch.unsqueeze(torch.tensor(data[1]).cpu(), 0).to(device).float(), + ] + else: + tensor = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device).float() + inputs = [tensor, tensor] + + with torch.no_grad(): + output = model(inputs) + _, predict = torch.max(output.data, 1) + + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print("PREDICTION" + shablon + ": " + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + cv2.destroyAllWindows() + gc.collect() + print("Уверенность" + shablon + " в предсказании: " + str(probability)) + print("Инференс завершен") + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_ensemble(src="", model_type="", prediction="", model_id=0, ind_inference=0, data=None): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + + if int(ind_inference) <= 100 and isinstance(data, (list, tuple)) and len(data) >= 2: + fig, ax = plt.subplots() + ax.imshow(np.moveaxis(data[0], 0, -1)) + plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_real_" + str(model_id) + "_" + model_type + ".png") + plt.clf() + plt.cla() + plt.close(fig) + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(np.moveaxis(data[1], 0, -1)) + plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_mod_" + str(model_id) + "_" + model_type + ".png") + plt.clf() + plt.cla() + plt.close(fig) + cv2.destroyAllWindows() + gc.collect() + + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print("Постобработка завершена") + print() + + except Exception as exc: + print(str(exc)) + return None diff --git a/NN_server/Models/ensemble_915_v44.py b/NN_server/Models/ensemble_915_v44.py new file mode 100644 index 0000000..d805a4f --- /dev/null +++ b/NN_server/Models/ensemble_915_v44.py @@ -0,0 +1,197 @@ +from torchvision import models +import torch.nn as nn +import matplotlib +import numpy as np +import torch +import cv2 +import gc +import io + + +def _render_plot(values, figsize=(16, 16), dpi=16): + import matplotlib.pyplot as plt + + fig = plt.figure(figsize=figsize) + plt.axes(ylim=(-1, 1)) + plt.plot(values, color="black") + plt.gca().set_axis_off() + plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) + plt.margins(0, 0) + + buf = io.BytesIO() + fig.savefig(buf, format="png", dpi=dpi) + buf.seek(0) + img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8) + buf.close() + + img = cv2.imdecode(img_arr, 1) + if img is None: + raise RuntimeError("failed to decode plot image") + + plt.clf() + plt.cla() + plt.close() + plt.close(fig) + + return np.asarray(cv2.split(img), dtype=np.float32) + + +def pre_func_ensemble(data=None, src="", ind_inference=0): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + + real = np.asarray(data[0], dtype=np.float32) + imag = np.asarray(data[1], dtype=np.float32) + signal = real + 1j * imag + + img_real = _render_plot(signal.real) + img_mag = _render_plot(np.abs(signal)) + + cv2.destroyAllWindows() + gc.collect() + + print("Подготовка данных завершена") + print() + return [img_real, img_mag] + + except Exception as exc: + print(str(exc)) + return None + + +def build_func_ensemble(file_model="", file_config="", num_classes=None): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + torch.cuda.empty_cache() + + num_classes = 2 + model1 = models.resnet18(pretrained=False) + model2 = models.resnet50(pretrained=False) + + model1.fc = nn.Linear(model1.fc.in_features, num_classes) + model2.fc = nn.Linear(model2.fc.in_features, num_classes) + + class Ensemble(nn.Module): + def __init__(self, model1, model2): + super().__init__() + self.model1 = model1 + self.model2 = model2 + self.fc = nn.Linear(2 * num_classes, num_classes) + + def forward(self, x): + if isinstance(x, (list, tuple)): + x1 = x[0] + x2 = x[1] if len(x) > 1 else x[0] + else: + x1 = x + x2 = x + y1 = self.model1(x1) + y2 = self.model2(x2) + y = torch.cat((y1, y2), dim=1) + return self.fc(y) + + model = Ensemble(model1, model2) + + device = "cuda" if torch.cuda.is_available() else "cpu" + if device != "cpu": + model = model.to(device) + model.load_state_dict(torch.load(file_model, map_location=device)) + model.eval() + + cv2.destroyAllWindows() + gc.collect() + + print("Инициализация модели завершена") + print() + return model + + except Exception as exc: + print(str(exc)) + return None + + +def inference_func_ensemble(data=None, model=None, mapping=None, shablon=""): + try: + cv2.destroyAllWindows() + gc.collect() + torch.cuda.empty_cache() + + device = "cuda" if torch.cuda.is_available() else "cpu" + if isinstance(data, (list, tuple)) and len(data) >= 2: + inputs = [ + torch.unsqueeze(torch.tensor(data[0]).cpu(), 0).to(device).float(), + torch.unsqueeze(torch.tensor(data[1]).cpu(), 0).to(device).float(), + ] + else: + tensor = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device).float() + inputs = [tensor, tensor] + + with torch.no_grad(): + output = model(inputs) + _, predict = torch.max(output.data, 1) + + prediction = mapping[int(np.asarray(predict.cpu())[0])] + print("PREDICTION" + shablon + ": " + prediction) + + output = output.cpu() + label = np.asarray(np.argmax(output, axis=1))[0] + output = np.asarray(torch.squeeze(output, 0)) + expon = np.exp(output - np.max(output)) + probability = round((expon / expon.sum())[label], 2) + + cv2.destroyAllWindows() + gc.collect() + print("Уверенность" + shablon + " в предсказании: " + str(probability)) + print("Инференс завершен") + print() + return [prediction, probability] + + except Exception as exc: + print(str(exc)) + return None + + +def post_func_ensemble(src="", model_type="", prediction="", model_id=0, ind_inference=0, data=None): + try: + import matplotlib.pyplot as plt + + matplotlib.use("Agg") + plt.ioff() + + if int(ind_inference) <= 100 and isinstance(data, (list, tuple)) and len(data) >= 2: + fig, ax = plt.subplots() + ax.imshow(np.moveaxis(data[0], 0, -1)) + plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_real_" + str(model_id) + "_" + model_type + ".png") + plt.clf() + plt.cla() + plt.close(fig) + cv2.destroyAllWindows() + gc.collect() + + fig, ax = plt.subplots() + ax.imshow(np.moveaxis(data[1], 0, -1)) + plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_mod_" + str(model_id) + "_" + model_type + ".png") + plt.clf() + plt.cla() + plt.close(fig) + cv2.destroyAllWindows() + gc.collect() + + plt.clf() + plt.cla() + plt.close() + cv2.destroyAllWindows() + gc.collect() + + print("Постобработка завершена") + print() + + except Exception as exc: + print(str(exc)) + return None From 523dbf77d19ddf96bd6a22cead54d60637563db5 Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Thu, 9 Apr 2026 11:22:12 +0700 Subject: [PATCH 5/8] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20drone=5Fstreaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NN_server/server.py | 51 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/NN_server/server.py b/NN_server/server.py index d5d1c06..067f849 100644 --- a/NN_server/server.py +++ b/NN_server/server.py @@ -3,6 +3,7 @@ from dotenv import dotenv_values from common.runtime import load_root_env, validate_env, as_int, as_str import os import sys +import re import matplotlib.pyplot as plt from Model import Model import numpy as np @@ -49,13 +50,47 @@ validate_env("NN_server/server.py", { }) config = dict(dotenv_values(ROOT_ENV)) + +def is_model_config_key(key, value): + return bool(re.fullmatch(r"NN_\d+", key or "")) and isinstance(value, str) and " && " in value + + +def get_required_drone_streak(freq): + raw_value = config.get(f"DRONE_STREAK_{freq}", "1") + try: + return max(1, int(raw_value)) + except (TypeError, ValueError): + logging.warning("Invalid DRONE_STREAK_%s=%r, falling back to 1", freq, raw_value) + return 1 + + +def update_drone_streak(freq, prediction): + if prediction == "drone": + drone_streaks[freq] = drone_streaks.get(freq, 0) + 1 + else: + drone_streaks[freq] = 0 + + required = get_required_drone_streak(freq) + triggered = prediction == "drone" and drone_streaks[freq] >= required + logging.info( + "NN alarm gate freq=%s prediction=%s streak=%s/%s triggered=%s", + freq, + prediction, + drone_streaks[freq], + required, + triggered, + ) + return 8 if triggered else 0 + + if not config: raise RuntimeError("[NN_server/server.py] .env was loaded but no keys were parsed") -if not any(key.startswith("NN_") for key in config): +if not any(is_model_config_key(key, value) for key, value in config.items()): raise RuntimeError("[NN_server/server.py] no NN_* model entries configured") logging.info("NN config loaded from %s", ROOT_ENV) gen_server_ip = config['GENERAL_SERVER_IP'] gen_server_port = config['GENERAL_SERVER_PORT'] +drone_streaks = {} def init_data_for_inference(): try: @@ -71,9 +106,9 @@ def init_data_for_inference(): try: global model_list - for key in config.keys(): - if key.startswith('NN_'): - params = config[key].split(' && ') + for key, value in config.items(): + if is_model_config_key(key, value): + params = value.split(' && ') module = importlib.import_module('Models.' + params[4]) classes = {} for value in params[9][1:-1].split(','): @@ -137,13 +172,7 @@ def receive_data(): print() try: - result = 0 - if (int(freq) == 2400 and (prediction_list[0] in ['drone', 'drone_noise'] or (prediction_list[0] == 'wifi' and float(probability) >= 0.95))) or (int(freq) == 1200 and (prediction_list[0] in ['drone'] and float(probability) >= 0.95)): - result += 8 - if int(freq) in [915]: - result = 0 - if int(freq) in []: - result = 8 + result = update_drone_streak(freq, prediction_list[0]) data_to_send={ 'freq': str(freq), 'amplitude': result From efd1d6e809a214fe985c5c921f6d82ae840c7958 Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Thu, 9 Apr 2026 11:22:39 +0700 Subject: [PATCH 6/8] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D1=87?= =?UTF-8?q?=D0=B5=D0=BA=D0=BF=D0=BE=D0=B8=D0=BD=D1=82=D1=8B=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c280524..20ca32f 100644 --- a/.gitignore +++ b/.gitignore @@ -144,6 +144,7 @@ celerybeat.pid # Environments .env +.env.bak* .venv env/ venv/ @@ -186,4 +187,11 @@ cython_debug/ /logs/*.log runtime/ -/.venv-*/* \ No newline at end of file +/.venv-*/* + +/models/ensemble_*/ + +NN_server/server.py.bak_streak_gate + +*.npy +train_scripts/models/ensemble*/ \ No newline at end of file From 75a9086fa5c904f535492cd32aa2668cdee3793d Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Thu, 9 Apr 2026 11:22:53 +0700 Subject: [PATCH 7/8] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=BD=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D1=83?= =?UTF-8?q?=20=D1=87=D0=B5=D0=BA=D0=BF=D0=BE=D0=B8=D0=BD=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20=D0=B4=D0=BE=D0=BA=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/docker/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 50038db..e217370 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -50,6 +50,7 @@ services: - ../../.env:/app/.env:ro - ../../NN_server:/app/NN_server - ../../common:/app/common + - ../../train_scripts:/app/train_scripts:ro gpus: all networks: - dronedetector-net From 73b0bc3298743721fb83c3e18b0178cd8f55b3e2 Mon Sep 17 00:00:00 2001 From: Sergey Revyakin Date: Thu, 9 Apr 2026 11:29:05 +0700 Subject: [PATCH 8/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20=D0=B4=D0=B8=D0=B0?= =?UTF-8?q?=D0=BF=D0=B0=D0=B7=D0=BE=D0=BD=D0=BE=D0=B2=20=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=BA=D0=B8=D0=BC=20=D1=81=D0=BA=D0=B0=D0=BD=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- read_energy_wide.py | 399 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 read_energy_wide.py diff --git a/read_energy_wide.py b/read_energy_wide.py new file mode 100644 index 0000000..a594cab --- /dev/null +++ b/read_energy_wide.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +import argparse +import math +import re +import signal +import subprocess +import sys +import time +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +try: + import numpy as np +except Exception as exc: + print(f"numpy import failed: {exc}", file=sys.stderr) + sys.exit(1) + +try: + from gnuradio import blocks, gr + import osmosdr +except Exception as exc: + print(f"gnuradio/osmosdr import failed: {exc}", file=sys.stderr) + print("Run with the SDR venv, e.g. .venv-sdr/bin/python read_energy_wide.py", file=sys.stderr) + sys.exit(1) + +EPS = 1e-20 + + +@dataclass +class ScanWindow: + seq: int + start_mhz: float + end_mhz: float + low_mhz: float + high_mhz: float + center_mhz: float + status: str = "INIT" + rms: Optional[float] = None + power_lin: Optional[float] = None + dbfs: Optional[float] = None + samples: int = 0 + updated_at: float = 0.0 + error: str = "" + pass_no: int = 0 + + +class WideProbeTop(gr.top_block): + def __init__( + self, + index: int, + center_freq_hz: float, + sample_rate: float, + vec_len: int, + gain: float, + if_gain: float, + bb_gain: float, + ): + super().__init__("hackrf_energy_wide_probe") + self.probe = blocks.probe_signal_vc(vec_len) + self.stream_to_vec = blocks.stream_to_vector(gr.sizeof_gr_complex * 1, vec_len) + self.src = osmosdr.source(args=f"numchan=1 hackrf={index}") + self.src.set_time_unknown_pps(osmosdr.time_spec_t()) + self.src.set_sample_rate(sample_rate) + self.src.set_center_freq(center_freq_hz, 0) + try: + self.src.set_freq_corr(0, 0) + except Exception: + pass + try: + self.src.set_gain_mode(False, 0) + except Exception: + pass + for fn, val in (("set_gain", gain), ("set_if_gain", if_gain), ("set_bb_gain", bb_gain)): + try: + getattr(self.src, fn)(val, 0) + except Exception: + pass + try: + self.src.set_bandwidth(0, 0) + except Exception: + pass + try: + self.src.set_antenna("", 0) + except Exception: + pass + self.connect((self.src, 0), (self.stream_to_vec, 0)) + self.connect((self.stream_to_vec, 0), (self.probe, 0)) + + def tune(self, freq_hz: float) -> None: + self.src.set_center_freq(freq_hz, 0) + + def read_metrics(self) -> Tuple[float, float, float, int]: + arr = np.asarray(self.probe.level(), dtype=np.complex64) + if arr.size == 0: + raise RuntimeError("no samples") + power_lin = float(np.mean(arr.real * arr.real + arr.imag * arr.imag)) + rms = math.sqrt(max(power_lin, 0.0)) + dbfs = 10.0 * math.log10(max(power_lin, EPS)) + return rms, power_lin, dbfs, int(arr.size) + + def read_window(self, settle: float, avg_reads: int, pause_between_reads: float) -> Tuple[float, float, float, int]: + if settle > 0: + time.sleep(settle) + + read_count = max(1, avg_reads) + powers: List[float] = [] + sample_sizes: List[int] = [] + last_error: Optional[Exception] = None + + for idx in range(read_count): + deadline = time.time() + 1.0 + while True: + try: + _, power_lin, _, samples = self.read_metrics() + powers.append(power_lin) + sample_sizes.append(samples) + break + except Exception as exc: + last_error = exc + if time.time() >= deadline: + raise RuntimeError(str(last_error) if last_error else "no samples") + time.sleep(0.02) + if idx + 1 < read_count and pause_between_reads > 0: + time.sleep(pause_between_reads) + + power_lin = float(sum(powers) / len(powers)) + rms = math.sqrt(max(power_lin, 0.0)) + dbfs = 10.0 * math.log10(max(power_lin, EPS)) + samples = int(sum(sample_sizes) / len(sample_sizes)) + return rms, power_lin, dbfs, samples + + +def parse_hackrf_info() -> Dict[str, int]: + try: + proc = subprocess.run(["hackrf_info"], capture_output=True, text=True, timeout=15) + except FileNotFoundError: + raise RuntimeError("hackrf_info not found") + except subprocess.TimeoutExpired: + raise RuntimeError("hackrf_info timeout") + text = (proc.stdout or "") + "\n" + (proc.stderr or "") + out: Dict[str, int] = {} + cur_idx: Optional[int] = None + for line in text.splitlines(): + m = re.search(r"^Index:\s*(\d+)", line) + if m: + cur_idx = int(m.group(1)) + continue + m = re.search(r"^Serial number:\s*([0-9a-fA-F]+)", line) + if m and cur_idx is not None: + out[m.group(1).lower()] = cur_idx + if not out: + raise RuntimeError("no devices parsed from hackrf_info") + return out + + +def fmt(value: Optional[float], spec: str) -> str: + return "-" if value is None else format(value, spec) + + +def build_windows(base_mhz: float, roof_mhz: float, step_mhz: float) -> List[ScanWindow]: + if step_mhz <= 0: + raise ValueError("step must be > 0") + if base_mhz == roof_mhz: + raise ValueError("base and roof must be different") + + direction = -1.0 if roof_mhz < base_mhz else 1.0 + edge = base_mhz + seq = 1 + windows: List[ScanWindow] = [] + + while True: + next_edge = edge + direction * step_mhz + if direction < 0 and next_edge < roof_mhz: + next_edge = roof_mhz + if direction > 0 and next_edge > roof_mhz: + next_edge = roof_mhz + + low_mhz = min(edge, next_edge) + high_mhz = max(edge, next_edge) + center_mhz = (low_mhz + high_mhz) / 2.0 + windows.append( + ScanWindow( + seq=seq, + start_mhz=edge, + end_mhz=next_edge, + low_mhz=low_mhz, + high_mhz=high_mhz, + center_mhz=center_mhz, + ) + ) + + if next_edge == roof_mhz: + break + edge = next_edge + seq += 1 + + return windows + + +def render( + windows: List[ScanWindow], + serial: str, + index: int, + sample_rate: float, + base_mhz: float, + roof_mhz: float, + step_mhz: float, + started_at: float, + pass_no: int, + current_seq: int, +) -> None: + now = time.time() + capture_bw_mhz = sample_rate / 1e6 + current_row = next((row for row in windows if row.seq == current_seq), None) + best_row = max( + (row for row in windows if row.status == "OK" and row.dbfs is not None), + key=lambda row: row.dbfs if row.dbfs is not None else float("-inf"), + default=None, + ) + print("\x1b[2J\x1b[H", end="") + print("HackRF Wide Energy Monitor (relative power: RMS / linear / dBFS)") + print( + f"serial: {serial} | idx: {index} | sample-rate: {capture_bw_mhz:.3f} MHz | " + f"scan: {base_mhz:.3f}->{roof_mhz:.3f} MHz step {step_mhz:.3f} MHz | " + f"pass: {pass_no} | uptime: {int(now-started_at)}s | {time.strftime('%Y-%m-%d %H:%M:%S')}" + ) + print() + header = ( + f"{'cur':>3} {'seq':>3} {'window MHz':>23} {'center':>9} {'status':>8} " + f"{'dBFS':>9} {'rms':>10} {'power':>12} {'N':>5} {'age':>5} error" + ) + print(header) + print("-" * len(header)) + for row in windows: + age = "-" if row.updated_at <= 0 else f"{(now-row.updated_at):.1f}" + err = row.error + if len(err) > 50: + err = err[:47] + "..." + marker = ">>>" if row.seq == current_seq else "" + print( + f"{marker:>3} {row.seq:>3} " + f"{f'{row.high_mhz:.3f}-{row.low_mhz:.3f}':>23} {row.center_mhz:>9.3f} {row.status:>8} " + f"{fmt(row.dbfs, '.2f'):>9} {fmt(row.rms, '.6f'):>10} {fmt(row.power_lin, '.8f'):>12} " + f"{row.samples:>5} {age:>5} {err}" + ) + print() + if best_row is not None: + best_age = "-" if best_row.updated_at <= 0 else f"{(now-best_row.updated_at):.1f}" + print( + f"{'':>3} {'MAX':>3} " + f"{f'{best_row.high_mhz:.3f}-{best_row.low_mhz:.3f}':>23} {best_row.center_mhz:>9.3f} {best_row.status:>8} " + f"{fmt(best_row.dbfs, '.2f'):>9} {fmt(best_row.rms, '.6f'):>10} {fmt(best_row.power_lin, '.8f'):>12} " + f"{best_row.samples:>5} {best_age:>5} pass={best_row.pass_no}" + ) + elif current_row is not None: + current_age = "-" if current_row.updated_at <= 0 else f"{(now-current_row.updated_at):.1f}" + print( + f"{'':>3} {'MAX':>3} " + f"{f'{current_row.high_mhz:.3f}-{current_row.low_mhz:.3f}':>23} {current_row.center_mhz:>9.3f} {'INIT':>8} " + f"{fmt(None, '.2f'):>9} {fmt(None, '.6f'):>10} {fmt(None, '.8f'):>12} " + f"{0:>5} {current_age:>5} no successful windows yet" + ) + print("Ctrl+C to stop. Window width equals step; sample-rate must be >= step to cover each window.") + sys.stdout.flush() + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Retune one HackRF across a wide frequency range and measure energy") + parser.add_argument("--serial", required=True, help="HackRF serial number from hackrf_info") + parser.add_argument("--sample-rate", type=float, required=True, help="Sample rate in Hz") + parser.add_argument("--base", type=float, required=True, help="Scan start edge in MHz") + parser.add_argument("--roof", type=float, required=True, help="Scan end edge in MHz") + parser.add_argument("--step", type=float, required=True, help="Window width / retune step in MHz") + parser.add_argument("--vec-len", type=int, default=4096, help="Probe vector length") + parser.add_argument("--settle", type=float, default=0.12, help="Wait time after retune before reading (s)") + parser.add_argument("--avg-reads", type=int, default=3, help="How many probe reads to average per window") + parser.add_argument("--pause-between-reads", type=float, default=0.02, help="Pause between averaged reads (s)") + parser.add_argument("--passes", type=int, default=0, help="Number of sweep passes, 0 means infinite") + parser.add_argument("--gain", type=float, default=16.0, help="General gain") + parser.add_argument("--if-gain", type=float, default=16.0, help="IF gain") + parser.add_argument("--bb-gain", type=float, default=16.0, help="BB gain") + return parser + + +def main() -> int: + args = build_parser().parse_args() + serial = args.serial.lower() + + try: + windows = build_windows(args.base, args.roof, args.step) + except ValueError as exc: + print(f"invalid scan range: {exc}", file=sys.stderr) + return 2 + + step_hz = args.step * 1e6 + if args.sample_rate < step_hz: + print( + f"sample-rate {args.sample_rate:.0f} Hz is smaller than step window {step_hz:.0f} Hz; " + "this would leave gaps in the scan", + file=sys.stderr, + ) + return 2 + + try: + serial_to_index = parse_hackrf_info() + except Exception as exc: + print(f"hackrf discovery failed: {exc}", file=sys.stderr) + return 3 + + index = serial_to_index.get(serial) + if index is None: + print(f"serial {serial} not found in hackrf_info", file=sys.stderr) + print("available serials:", file=sys.stderr) + for item_serial, item_index in sorted(serial_to_index.items(), key=lambda item: item[1]): + print(f" idx={item_index} serial={item_serial}", file=sys.stderr) + return 4 + + stop_requested = False + + def on_signal(signum, frame): + nonlocal stop_requested + stop_requested = True + + signal.signal(signal.SIGINT, on_signal) + signal.signal(signal.SIGTERM, on_signal) + + probe: Optional[WideProbeTop] = None + started_at = time.time() + pass_no = 0 + current_seq = windows[0].seq + + try: + probe = WideProbeTop( + index=index, + center_freq_hz=windows[0].center_mhz * 1e6, + sample_rate=args.sample_rate, + vec_len=args.vec_len, + gain=args.gain, + if_gain=args.if_gain, + bb_gain=args.bb_gain, + ) + probe.start() + time.sleep(max(args.settle, 0.12)) + + while not stop_requested: + pass_no += 1 + for row in windows: + if stop_requested: + break + current_seq = row.seq + try: + probe.tune(row.center_mhz * 1e6) + rms, power_lin, dbfs, samples = probe.read_window( + settle=args.settle, + avg_reads=args.avg_reads, + pause_between_reads=args.pause_between_reads, + ) + row.status = "OK" + row.rms = rms + row.power_lin = power_lin + row.dbfs = dbfs + row.samples = samples + row.error = "" + row.updated_at = time.time() + row.pass_no = pass_no + except Exception as exc: + row.status = "ERR" + row.error = str(exc) + row.updated_at = time.time() + render( + windows=windows, + serial=serial, + index=index, + sample_rate=args.sample_rate, + base_mhz=args.base, + roof_mhz=args.roof, + step_mhz=args.step, + started_at=started_at, + pass_no=pass_no, + current_seq=current_seq, + ) + if args.passes > 0 and pass_no >= args.passes: + break + except Exception as exc: + print(f"scanner failed: {exc}", file=sys.stderr) + return 5 + finally: + if probe is not None: + try: + probe.stop() + probe.wait() + except Exception: + pass + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())