commit acf7a765469b83c102822f2d50378eaa0b2d8e90 Author: LittleSam129 <1qwe432@gmail.com> Date: Mon Jul 20 13:58:58 2026 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9e38f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# ========================================== +# SDR Rover Link +# Файлы, не включаемые в Git +# ========================================== + +# Python: скомпилированные и временные файлы +__pycache__/ +*.py[cod] +*$py.class + +# Кэш инструментов проверки и тестирования +.pytest_cache/ +.mypy_cache/ +.coverage + +# Spyder +.spyproject/ +.spyderproject +.spyderworkspace + +# Jupyter Notebook +.ipynb_checkpoints/ + +# Виртуальные окружения Python +.venv/ +venv/ +env/ + +# Временные файлы редакторов +*.tmp +*.temp +*.bak +*.swp +*~ + +# Временные файлы Microsoft Office +~$*.docx +~$*.xlsx +~$*.pptx + +# Системные файлы Windows +Thumbs.db +Desktop.ini + +# Системные файлы macOS +.DS_Store + +# Локальные настройки и секреты +.env +*.local + diff --git a/Projects/SDR_Rover/PROJECT_LOG.md b/Projects/SDR_Rover/PROJECT_LOG.md new file mode 100644 index 0000000..513ae58 --- /dev/null +++ b/Projects/SDR_Rover/PROJECT_LOG.md @@ -0,0 +1,35 @@ +# Инженерный журнал SDR Rover Link + +## Назначение журнала + +Этот файл содержит хронологию разработки проекта SDR Rover Link. + +В журнале фиксируются: + +- выполненные работы; +- полученные результаты; +- обнаруженные ошибки; +- принятые технические решения; +- результаты экспериментов; +- планы следующего этапа. + +Записи не следует удалять. Если решение изменилось, необходимо добавить новую запись и объяснить причину изменения. + +--- + +# Запись 001 + +## Дата + +10 июля 2026 года + +## Тема + +Создание структуры проекта и локального Git-репозитория. + +## Выполнено + +1. Проверена установленная версия Git: + +```text +git version 2.51.1.windows.1 \ No newline at end of file diff --git a/Projects/SDR_Rover/README.md b/Projects/SDR_Rover/README.md new file mode 100644 index 0000000..dbbeab6 --- /dev/null +++ b/Projects/SDR_Rover/README.md @@ -0,0 +1,340 @@ +# SDR Rover Link + +## 1. Назначение проекта + +**SDR Rover Link** — учебно-исследовательский проект по разработке цифрового радиоканала для наземного робототехнического комплекса с использованием программно-определяемого радио — SDR, Software Defined Radio. + +Проект должен пройти полный путь от передачи простого текстового пакета до передачи телеметрии и отдельных JPEG-изображений через реальный радиоканал. + +Основная задача проекта — не только получить работающую систему, но и последовательно изучить все этапы цифровой радиосвязи: + +- формирование данных; +- разбиение данных на пакеты; +- контроль целостности; +- исправление ошибок; +- цифровую модуляцию; +- формирование IQ-сигнала; +- передачу через SDR; +- приём и демодуляцию; +- восстановление исходных данных. + +--- + +## 2. Целевой сценарий + +Разрабатываемая система предназначена для исследовательского наземного ровера. + +Целевые требования: + +| Параметр | Требование | +|---|---| +| Дальность связи | до 10 км | +| Скорость ровера | до 25 км/ч | +| Прямая радиовидимость | может отсутствовать | +| Рабочие частоты | ниже 350 МГц | +| Основной тип данных | телеметрия и отдельные изображения | +| Формат изображения | JPEG | +| Частота передачи изображений | отдельные кадры, не видеопоток | +| Действие при потере связи | безопасная остановка ровера | + +Дальность 10 км без прямой видимости является целевым исследовательским требованием, а не гарантированной характеристикой первой версии системы. + +--- + +## 3. Почему не используется обычное FPV-видео + +Типовые FPV-системы работают преимущественно в диапазонах 2,4 и 5,8 ГГц. + +Для наземного робота такие диапазоны имеют ряд ограничений: + +- высокие потери при работе у поверхности земли; +- затенение рельефом, зданиями и растительностью; +- быстрое ухудшение изображения при слабом сигнале; +- высокая загруженность распространённых диапазонов; +- зависимость от постоянного широкополосного видеопотока. + +В этом проекте вместо непрерывного видео предполагается передавать отдельные сжатые изображения. + +Пример: + +```text +Камера + ↓ +JPEG-изображение + ↓ +Разбиение на пакеты + ↓ +Передача через SDR + ↓ +Сборка изображения на наземной станции +``` + +Такой подход позволяет существенно снизить требуемую скорость радиоканала. + +--- + +## 4. Текущее оборудование + +### SDR-оборудование + +- RTL-SDR — приёмник; +- ADALM-PlutoSDR — приёмопередатчик, ожидается поступление; +- HackRF One — может временно использоваться при необходимости. + +### Вычислительное оборудование + +- компьютер под управлением Windows; +- Raspberry Pi — планируется для установки на ровер; +- бортовой вычислитель ровера — будет выбран позднее. + +### Программное обеспечение + +- Python 3.12; +- Anaconda; +- Spyder; +- Git; +- GNU Radio — будет установлен на следующем этапе; +- libiio и драйверы PlutoSDR — будут установлены после получения устройства. + +--- + +## 5. Предварительно рассматриваемые диапазоны + +В рамках проекта рассматриваются: + +- 27 МГц; +- 50 МГц; +- 144 МГц; +- другие разрешённые для лабораторных испытаний частоты ниже 350 МГц. + +Конкретный диапазон будет выбран после анализа: + +- доступной полосы; +- требований законодательства; +- размеров антенны; +- потерь распространения; +- уровня внешних помех; +- возможностей выходного радиочастотного тракта; +- допустимой мощности передачи. + +Передача по эфиру должна выполняться только на разрешённых частотах и с соблюдением допустимой мощности. Первые опыты следует проводить через аттенюатор, эквивалент нагрузки или в экранированной лабораторной конфигурации. + +--- + +## 6. Основные этапы проекта + +### Этап 0. Подготовка среды + +- создание репозитория; +- настройка Git; +- настройка Python и Spyder; +- подготовка структуры каталогов; +- ведение документации. + +### Этап 1. Пакетный протокол + +- структура пакета; +- типы сообщений; +- порядковый номер пакета; +- длина данных; +- CRC; +- кодирование и декодирование. + +### Этап 2. Виртуальный канал + +- передача пакетов между Python-программами; +- имитация потерь; +- имитация повреждения битов; +- измерение числа ошибочных пакетов. + +### Этап 3. Передача файлов + +- разбиение файла на фрагменты; +- нумерация фрагментов; +- подтверждение приёма; +- повторная передача; +- сборка исходного файла. + +### Этап 4. Передача изображений + +- загрузка JPEG; +- разбиение JPEG на пакеты; +- передача через виртуальный канал; +- восстановление изображения; +- обработка потерянных фрагментов. + +### Этап 5. Физический уровень + +- BPSK; +- QPSK; +- формирование символов; +- IQ-сэмплы; +- синхронизация; +- фильтрация; +- оценка BER и PER. + +### Этап 6. PlutoSDR + +- подключение устройства; +- передача несущей; +- передача тестовой последовательности; +- передача цифрового пакета; +- приём сигнала RTL-SDR или вторым SDR. + +### Этап 7. Радиоканал передачи изображений + +- передача JPEG через SDR; +- измерение времени доставки; +- испытания при различных уровнях сигнала; +- испытания при наличии помех; +- подбор скорости и модуляции. + +### Этап 8. Полевые испытания + +- испытания на малой мощности; +- испытания на малой дистанции; +- постепенное увеличение расстояния; +- испытания с ретранслятором; +- испытания на наземном ровере. + +--- + +## 7. Структура репозитория + +```text +SDR_Rover +│ +├── README.md +├── docs +├── protocol +├── rover +├── ground_station +├── gnuradio +├── tests +└── tools +``` + +### `docs` + +Документация, лабораторные работы, схемы и отчёты. + +### `protocol` + +Реализация пакетного протокола: + +- структура пакета; +- CRC; +- кодирование; +- декодирование; +- типы сообщений. + +### `rover` + +Программы, которые будут работать на борту ровера: + +- получение изображения; +- сжатие JPEG; +- передача телеметрии; +- формирование радиопакетов. + +### `ground_station` + +Программы наземной станции: + +- приём пакетов; +- сборка изображений; +- отображение телеметрии; +- управление передачей. + +### `gnuradio` + +Схемы GNU Radio Companion: + +- передатчик; +- приёмник; +- BPSK-модем; +- QPSK-модем; +- тестовые генераторы. + +### `tests` + +Автоматические и лабораторные тесты. + +### `tools` + +Вспомогательные программы: + +- анализ пакетов; +- генераторы тестовых данных; +- построение графиков; +- анализ журналов. + +--- + +## 8. Принципы разработки + +В проекте применяются следующие правила: + +1. Каждый новый термин сначала объясняется, затем используется. +2. Каждый этап должен завершаться работающим результатом. +3. Радиоканал и пакетный протокол разрабатываются отдельно. +4. Код проверяется автоматическими тестами. +5. Все существенные решения фиксируются в документации. +6. Сложность увеличивается постепенно. +7. Испытания по эфиру начинаются только после проверки через виртуальный канал. +8. Передатчик сначала проверяется через аттенюатор или эквивалент нагрузки. + +--- + +## 9. Текущий статус + +Выполнено: + +- установлен Python; +- установлена Anaconda; +- выбран Spyder; +- установлен Git; +- создан локальный Git-репозиторий; +- создана базовая структура каталогов. + +Следующая задача: + +> Создать первый Python-модуль пакетного протокола и передать сообщение через виртуальный канал без SDR-оборудования. + +--- + +## 10. Ближайший контрольный результат + +Первая рабочая версия должна выполнять следующий цикл: + +```text +Текстовое сообщение + ↓ +Преобразование в байты + ↓ +Формирование пакета + ↓ +Добавление CRC + ↓ +Имитация радиоканала + ↓ +Проверка CRC + ↓ +Восстановление сообщения +``` + +Первое тестовое сообщение: + +```text +HELLO SDR +``` + +--- + +## 11. Автор + +Антон + +НИЯУ МИФИ + +2026 \ No newline at end of file diff --git a/Projects/SDR_Rover/data/processed/lab010/01_color_640_q40.jpg b/Projects/SDR_Rover/data/processed/lab010/01_color_640_q40.jpg new file mode 100644 index 0000000..ce00e9a Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/01_color_640_q40.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/02_gray_640_q40.jpg b/Projects/SDR_Rover/data/processed/lab010/02_gray_640_q40.jpg new file mode 100644 index 0000000..aa737d4 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/02_gray_640_q40.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/03_gray_320_q30.jpg b/Projects/SDR_Rover/data/processed/lab010/03_gray_320_q30.jpg new file mode 100644 index 0000000..e81436a Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/03_gray_320_q30.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/04_gray_320_q15.jpg b/Projects/SDR_Rover/data/processed/lab010/04_gray_320_q15.jpg new file mode 100644 index 0000000..679798b Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/04_gray_320_q15.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/05_edges_320_q40.jpg b/Projects/SDR_Rover/data/processed/lab010/05_edges_320_q40.jpg new file mode 100644 index 0000000..da083b7 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/05_edges_320_q40.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/06_gray_edges_320_q30.jpg b/Projects/SDR_Rover/data/processed/lab010/06_gray_edges_320_q30.jpg new file mode 100644 index 0000000..bd0de27 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/06_gray_edges_320_q30.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/lab010_comparison.jpg b/Projects/SDR_Rover/data/processed/lab010/lab010_comparison.jpg new file mode 100644 index 0000000..caad703 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab010/lab010_comparison.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab010/lab010_results.csv b/Projects/SDR_Rover/data/processed/lab010/lab010_results.csv new file mode 100644 index 0000000..aa1cc04 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab010/lab010_results.csv @@ -0,0 +1,8 @@ +name,path,width,height,mode,file_size_bytes,fragment_count,total_radio_bytes,efficiency_percent,time_5_kbps,time_10_kbps,time_20_kbps,time_50_kbps,time_100_kbps,time_20_kbps_with_retries_x2 +00_original,data\raw\lab009_source.jpg,640,960,RGB,101703,199,108867,93.42,174.1872,87.0936,43.5468,17.41872,8.70936,87.0936 +01_color_640_q40,data\processed\lab010\01_color_640_q40.jpg,320,480,RGB,10610,21,11366,93.35,18.1856,9.0928,4.5464,1.81856,0.90928,9.0928 +02_gray_640_q40,data\processed\lab010\02_gray_640_q40.jpg,320,480,L,9496,19,10180,93.28,16.288,8.144,4.072,1.6288,0.8144,8.144 +03_gray_320_q30,data\processed\lab010\03_gray_320_q30.jpg,160,240,L,2792,6,3008,92.82,4.8128,2.4064,1.2032,0.48128,0.24064,2.4064 +04_gray_320_q15,data\processed\lab010\04_gray_320_q15.jpg,160,240,L,1806,4,1950,92.62,3.12,1.56,0.78,0.312,0.156,1.56 +05_edges_320_q40,data\processed\lab010\05_edges_320_q40.jpg,160,240,L,6409,13,6877,93.19,11.0032,5.5016,2.7508,1.10032,0.55016,5.5016 +06_gray_edges_320_q30,data\processed\lab010\06_gray_edges_320_q30.jpg,160,240,L,2185,5,2365,92.39,3.784,1.892,0.946,0.3784,0.1892,1.892 diff --git a/Projects/SDR_Rover/data/processed/lab011/01_gray_320_q30.jpg b/Projects/SDR_Rover/data/processed/lab011/01_gray_320_q30.jpg new file mode 100644 index 0000000..b65dd33 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab011/01_gray_320_q30.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab011/02_gray_320_q15.jpg b/Projects/SDR_Rover/data/processed/lab011/02_gray_320_q15.jpg new file mode 100644 index 0000000..51ed385 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab011/02_gray_320_q15.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab011/03_gray_edges_320_q30.jpg b/Projects/SDR_Rover/data/processed/lab011/03_gray_edges_320_q30.jpg new file mode 100644 index 0000000..fd14487 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab011/03_gray_edges_320_q30.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab011/lab011_comparison.jpg b/Projects/SDR_Rover/data/processed/lab011/lab011_comparison.jpg new file mode 100644 index 0000000..406f0fa Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab011/lab011_comparison.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab012/01_color_320_q30.jpg b/Projects/SDR_Rover/data/processed/lab012/01_color_320_q30.jpg new file mode 100644 index 0000000..b526181 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab012/01_color_320_q30.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab012/02_gray_320_q30.jpg b/Projects/SDR_Rover/data/processed/lab012/02_gray_320_q30.jpg new file mode 100644 index 0000000..c8b7cf7 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab012/02_gray_320_q30.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab012/03_color_320_q15.jpg b/Projects/SDR_Rover/data/processed/lab012/03_color_320_q15.jpg new file mode 100644 index 0000000..cae67b6 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab012/03_color_320_q15.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab012/04_gray_320_q15.jpg b/Projects/SDR_Rover/data/processed/lab012/04_gray_320_q15.jpg new file mode 100644 index 0000000..a775b48 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab012/04_gray_320_q15.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab012/lab012_comparison.jpg b/Projects/SDR_Rover/data/processed/lab012/lab012_comparison.jpg new file mode 100644 index 0000000..c3eb5ae Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab012/lab012_comparison.jpg differ diff --git a/Projects/SDR_Rover/data/processed/lab013/lab013_bpsk_constellation.png b/Projects/SDR_Rover/data/processed/lab013/lab013_bpsk_constellation.png new file mode 100644 index 0000000..afe63b8 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab013/lab013_bpsk_constellation.png differ diff --git a/Projects/SDR_Rover/data/processed/lab014/lab014_bpsk_ber_curve.png b/Projects/SDR_Rover/data/processed/lab014/lab014_bpsk_ber_curve.png new file mode 100644 index 0000000..9af6ae4 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab014/lab014_bpsk_ber_curve.png differ diff --git a/Projects/SDR_Rover/data/processed/lab014/lab014_bpsk_ber_results.csv b/Projects/SDR_Rover/data/processed/lab014/lab014_bpsk_ber_results.csv new file mode 100644 index 0000000..8a17b99 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab014/lab014_bpsk_ber_results.csv @@ -0,0 +1,10 @@ +eb_n0_db,bit_errors,experimental_ber,theoretical_ber,short_packet_per_224_bits,image_packet_per_4288_bits +-4.0,186789,0.186789,0.186113817483389,1.0,1.0 +-2.0,130895,0.130895,0.13064448852282923,0.999999999999976,1.0 +0.0,78830,0.07883,0.07864960352514255,0.9999999892561172,1.0 +2.0,37551,0.037551,0.03750612835892598,0.9998089504830603,1.0 +4.0,12379,0.012379,0.012500818040737556,0.9402647377229729,1.0 +6.0,2266,0.002266,0.002388290780932807,0.4146919530687869,0.9999647568582152 +8.0,205,0.000205,0.00019090777407599314,0.041865796670504794,0.5589913040105388 +10.0,5,5e-06,3.872108215522038e-06,0.0008669778768398397,0.016466551600075156 +12.0,0,0.0,9.006010350628754e-09,2.017344292783227e-06,3.8617026900812036e-05 diff --git a/Projects/SDR_Rover/data/processed/lab015/lab015_effective_goodput.png b/Projects/SDR_Rover/data/processed/lab015/lab015_effective_goodput.png new file mode 100644 index 0000000..a52ed08 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab015/lab015_effective_goodput.png differ diff --git a/Projects/SDR_Rover/data/processed/lab015/lab015_fragment_results.csv b/Projects/SDR_Rover/data/processed/lab015/lab015_fragment_results.csv new file mode 100644 index 0000000..3861db2 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab015/lab015_fragment_results.csv @@ -0,0 +1,21 @@ +fragment_size,fragment_count,eb_n0_db,ber,ideal_total_bytes,ideal_efficiency_percent,ack_success_probability,minimum_data_success_probability,maximum_data_success_probability,average_data_transmissions,expected_total_bytes,expected_transfer_seconds,expected_efficiency_percent,effective_goodput_bps +64,1590,6.0,0.002388290780932807,158943,63.98708971140598,0.7948911114376548,0.18574738070642127,0.5526642915589317,6.769993990326338,971131.0972436845,388.45243889747377,10.472633436274343,2094.5266872548686 +64,1590,8.0,0.00019090777407599314,158943,63.98708971140598,0.9818380567064475,0.8742298920810428,0.9537538613227441,1.1649618711080167,182373.53560599152,72.94941424239661,55.766314812103005,11153.2629624206 +64,1590,10.0,3.872108215522038e-06,158943,63.98708971140598,0.9996283459721081,0.9972777426196243,0.9990401762291837,1.0031013824212691,159383.9632486371,63.75358529945484,63.81005838168582,12762.011676337162 +64,1590,12.0,9.006010350628754e-09,158943,63.98708971140598,0.9999991354233762,0.9999936597887839,0.9999977665119172,1.0000072022513673,158944.0240585637,63.577609623425474,63.98667744974611,12797.335489949222 +128,795,6.0,0.002388290780932807,130323,78.0391795768974,0.7948911114376548,0.0546047112997041,0.1624683695213351,23.01968691435649,2793259.2450467222,1117.303698018689,3.6410154259884613,728.2030851976923 +128,795,8.0,0.00019090777407599314,130323,78.0391795768974,0.9818380567064475,0.792814493179396,0.8649325435241314,1.2845263164977094,164871.50993740102,65.9486039749604,61.68621858234631,12337.243716469262 +128,795,10.0,3.872108215522038e-06,130323,78.0391795768974,0.9996283459721081,0.995302574868459,0.9970615178736658,1.005090911661357,130941.54342108239,52.37661736843295,77.67053705250979,15534.10741050196 +128,795,12.0,9.006010350628754e-09,130323,78.0391795768974,0.9999991354233762,0.9999890487513297,0.9999931554555267,1.0000118107896643,130324.43502447128,52.12977400978851,78.03832027424713,15607.664054849427 +256,398,6.0,0.002388290780932807,116031,87.65157587196525,0.7948911114376548,0.0047189453652365446,0.1624683695213351,265.9418083130096,29641130.986424245,11856.452394569698,0.3431144379969185,68.62288759938369 +256,398,8.0,0.00019090777407599314,116031,87.65157587196525,0.9818380567064475,0.6520237845186864,0.8649325435241314,1.5610902562146585,178614.3981088735,71.44575924354939,56.939978566569685,11387.995713313936 +256,398,10.0,3.872108215522038e-06,116031,87.65157587196525,0.9996283459721081,0.9913639674291386,0.9970615178736658,1.00907180620962,117043.12355799062,46.81724942319625,86.89361400168875,17378.72280033775 +256,398,12.0,9.006010350628754e-09,116031,87.65157587196525,0.9999991354233762,0.9999798267402067,0.9999931554555267,1.0000210047714104,116033.34347475081,46.41333738990033,87.64980561137658,17529.961122275316 +512,199,6.0,0.002388290780932807,108867,93.41949351043016,0.7948911114376548,3.52431417847955e-05,0.001213384631398025,35521.68507371966,3788696213.3825846,1515478.4853530338,0.002684379909921534,0.5368759819843069 +512,199,8.0,0.00019090777407599314,108867,93.41949351043016,0.9818380567064475,0.4410086959894613,0.58501358722673,2.3066167669494577,248143.0622186174,99.25722488744695,40.98563106729065,8197.126213458128 +512,199,10.0,3.872108215522038e-06,108867,93.41949351043016,0.9996283459721081,0.9835334483999248,0.9891859954162041,1.017091048475429,110688.79543911858,44.275518175647434,91.88192860580818,18376.385721161638 +512,199,12.0,9.006010350628754e-09,108867,93.41949351043016,0.9999991354233762,0.9999613829730992,0.9999747114425825,1.000039416147422,108871.20151010722,43.54848060404289,93.41588830592474,18683.177661184945 +1024,100,6.0,0.002388290780932807,105303,96.5812939802285,0.7948911114376548,1.9657756573348356e-09,0.001213384631398025,633568545.9977559,66397982899426.766,26559193159.770706,1.5317182173146714e-07,3.063436434629343e-05 +1024,100,8.0,0.00019090777407599314,105303,96.5812939802285,0.9818380567064475,0.20175048295563117,0.58501358722673,5.0152314190831655,525604.9860141345,210.2419944056538,19.349702287121193,3869.940457424238 +1024,100,10.0,3.872108215522038e-06,105303,96.5812939802285,0.9996283459721081,0.9680574753267193,0.9891859954162041,1.033159851833223,108770.71688930437,43.50828675572175,93.50218782092135,18700.43756418427 +1024,100,12.0,9.006010350628754e-09,105303,96.5812939802285,0.9999991354233762,0.9999244964594162,0.9999747114425825,1.0000758716835711,105310.93416073515,42.12437366429406,96.57401751348212,19314.803502696424 diff --git a/Projects/SDR_Rover/data/processed/lab015/lab015_transfer_time.png b/Projects/SDR_Rover/data/processed/lab015/lab015_transfer_time.png new file mode 100644 index 0000000..5a327d5 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab015/lab015_transfer_time.png differ diff --git a/Projects/SDR_Rover/data/processed/lab016/lab016_adaptation_results.csv b/Projects/SDR_Rover/data/processed/lab016/lab016_adaptation_results.csv new file mode 100644 index 0000000..9d8b83e --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab016/lab016_adaptation_results.csv @@ -0,0 +1,12 @@ +step,eb_n0_db,mode,fragment_size,fragment_count,expected_seconds,effective_goodput_bps,success_with_retries,expected_attempts,reason +0,12.0,1024 B,1024,4,1.50130532861719,19231.264586660185,1.0,1.0000763738844538,Выбран режим с максимальной ожидаемой полезной скоростью +1,10.0,1024 B,1024,4,1.5472051612507218,18660.744368678683,0.9999999648303772,1.0333805767765363,Выбран режим с максимальной ожидаемой полезной скоростью +2,9.0,512 B,512,8,1.797643784171777,16061.024021676303,0.9999516109774961,1.158845496802822,Выбран режим с максимальной ожидаемой полезной скоростью +3,8.0,128 B,128,29,2.3502958310297126,12284.41101703805,0.9994658078956576,1.2846610523483055,Выбран режим с максимальной ожидаемой полезной скоростью +4,7.0,128 B,128,29,4.872011036339501,5926.09495024717,0.8948830865552501,2.7569946980068387,Выбран режим с максимальной ожидаемой полезной скоростью +5,6.0,IMAGE OFF,0,0,,0.0,0.0,0.0,Канал недостаточен для надёжной передачи изображения +6,7.0,128 B,128,29,4.872011036339501,5926.09495024717,0.8948830865552501,2.7569946980068387,Выбран режим с максимальной ожидаемой полезной скоростью +7,8.0,128 B,128,29,2.3502958310297126,12284.41101703805,0.9994658078956576,1.2846610523483055,Выбран режим с максимальной ожидаемой полезной скоростью +8,9.0,512 B,512,8,1.797643784171777,16061.024021676303,0.9999516109774961,1.158845496802822,Выбран режим с максимальной ожидаемой полезной скоростью +9,10.0,1024 B,1024,4,1.5472051612507218,18660.744368678683,0.9999999648303772,1.0333805767765363,Выбран режим с максимальной ожидаемой полезной скоростью +10,12.0,1024 B,1024,4,1.50130532861719,19231.264586660185,1.0,1.0000763738844538,Выбран режим с максимальной ожидаемой полезной скоростью diff --git a/Projects/SDR_Rover/data/processed/lab016/lab016_expected_transfer_time.png b/Projects/SDR_Rover/data/processed/lab016/lab016_expected_transfer_time.png new file mode 100644 index 0000000..b5d4890 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab016/lab016_expected_transfer_time.png differ diff --git a/Projects/SDR_Rover/data/processed/lab016/lab016_selected_mode.png b/Projects/SDR_Rover/data/processed/lab016/lab016_selected_mode.png new file mode 100644 index 0000000..8a1b7cf Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab016/lab016_selected_mode.png differ diff --git a/Projects/SDR_Rover/data/processed/lab017/lab017_hysteresis_comparison.png b/Projects/SDR_Rover/data/processed/lab017/lab017_hysteresis_comparison.png new file mode 100644 index 0000000..373ea49 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab017/lab017_hysteresis_comparison.png differ diff --git a/Projects/SDR_Rover/data/processed/lab017/lab017_hysteresis_results.csv b/Projects/SDR_Rover/data/processed/lab017/lab017_hysteresis_results.csv new file mode 100644 index 0000000..026c13d --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab017/lab017_hysteresis_results.csv @@ -0,0 +1,46 @@ +step,measured_eb_n0_db,filtered_eb_n0_db,naive_mode,stable_mode,mode_changed,pending_upgrade_mode,pending_upgrade_count,reason +0,10.2,10.2,1024,1024,False,,0,Режим удерживается +1,9.7,10.024999999999999,1024,1024,False,,0,Режим удерживается +2,9.4,9.806249999999999,512,1024,False,,0,Режим удерживается +3,9.6,9.734062499999999,1024,1024,False,,0,Режим удерживается +4,9.3,9.582140625,512,1024,False,,0,Режим удерживается +5,9.7,9.62339140625,1024,1024,False,,0,Режим удерживается +6,9.2,9.4752044140625,512,1024,False,,0,Режим удерживается +7,9.6,9.518882869140624,1024,1024,False,,0,Режим удерживается +8,9.0,9.337273864941405,512,1024,False,,0,Режим удерживается +9,8.7,9.114228012211914,512,512,True,,0,Понижение по фильтрованному Eb/N0 +10,8.4,8.864248207937743,128,512,False,,0,Режим удерживается +11,8.6,8.771761335159534,512,512,False,,0,Режим удерживается +12,8.3,8.606644867853698,128,512,False,,0,Режим удерживается +13,8.7,8.639319164104903,512,512,False,,0,Режим удерживается +14,8.2,8.485557456668186,128,512,False,,0,Режим удерживается +15,8.6,8.525612346834322,512,512,False,,0,Режим удерживается +16,8.0,8.341648025442309,128,512,False,,0,Режим удерживается +17,7.8,8.1520712165375,128,512,False,,0,Режим удерживается +18,7.5,7.923846290749375,128,128,True,,0,Аварийное понижение по мгновенному Eb/N0 +19,7.7,7.8455000889870945,128,128,False,,0,Режим удерживается +20,7.4,7.689575057841611,128,128,False,,0,Режим удерживается +21,7.8,7.728223787597047,128,128,False,,0,Режим удерживается +22,7.2,7.543345461938081,128,128,False,,0,Режим удерживается +23,6.8,7.283174550259753,128,128,False,,0,Режим удерживается +24,6.5,7.009063457668839,128,128,False,,0,Режим удерживается +25,6.3,6.760891247484745,0,128,False,,0,Режим удерживается +26,6.6,6.704579310865084,128,128,False,,0,Режим удерживается +27,6.2,6.527976552062304,0,128,False,,0,Режим удерживается +28,6.7,6.588184758840497,128,128,False,,0,Режим удерживается +29,6.1,6.417320093246323,0,128,False,,0,Режим удерживается +30,5.5,6.0962580606101096,0,0,True,,0,Аварийное понижение по мгновенному Eb/N0 +31,6.0,6.062567739396571,0,0,False,,0,Режим удерживается +32,6.4,6.1806690306077705,0,0,False,,0,Режим удерживается +33,6.7,6.362434869895051,128,0,False,,0,Режим удерживается +34,6.9,6.550582665431783,128,0,False,,0,Режим удерживается +35,7.1,6.742878732530659,128,0,False,,0,Режим удерживается +36,7.3,6.937871176144928,128,0,False,128,1,Накопление подтверждений для повышения режима +37,8.0,7.3096162644942035,128,0,False,128,2,Накопление подтверждений для повышения режима +38,8.8,7.831250571921233,512,128,True,,0,Режим повышен после нескольких подтверждений +39,9.0,8.2403128717488,512,128,False,,0,Режим удерживается +40,9.4,8.64620336663672,512,128,False,,0,Режим удерживается +41,9.8,9.05003218831387,1024,128,False,512,1,Накопление подтверждений для повышения режима +42,10.0,9.382520922404016,1024,128,False,512,2,Накопление подтверждений для повышения режима +43,10.2,9.668638599562609,1024,512,True,,0,Режим повышен после нескольких подтверждений +44,10.1,9.819615089715695,1024,512,False,,0,Повышение запрещено: не выдержано минимальное время режима diff --git a/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_radio_frame.png b/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_radio_frame.png new file mode 100644 index 0000000..880cfae Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_radio_frame.png differ diff --git a/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.cf32 b/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.cf32 new file mode 100644 index 0000000..81b6674 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.cf32 differ diff --git a/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.npy b/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.npy new file mode 100644 index 0000000..c7ec0f6 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.npy differ diff --git a/Projects/SDR_Rover/data/processed/lab018/lab018_frame_info.txt b/Projects/SDR_Rover/data/processed/lab018/lab018_frame_info.txt new file mode 100644 index 0000000..853d9cf --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab018/lab018_frame_info.txt @@ -0,0 +1,21 @@ +Lab018. BPSK radio frame + +Message: ПРИВЕТ SDR +Protocol packet bytes: 28 +Preamble bits: 64 +Radio header bits: 32 +Protocol packet bits: 224 +Total radio frame bits: 320 +Symbol rate: 20000 symbols/s +Samples per symbol: 32 +Sample rate: 640000 samples/s +RRC rolloff: 0.35 +RRC taps: 321 +IQ sample count: 11584 +Frame duration: 0.018100 s +Approximate occupied bandwidth: 27000.0 Hz +Average normalized power: 0.177911 +Peak normalized power: 0.490000 +PAPR: 4.40 dB +NPY file: data\processed\lab018\lab018_bpsk_tx_iq.npy +CF32 file: data\processed\lab018\lab018_bpsk_tx_iq.cf32 \ No newline at end of file diff --git a/Projects/SDR_Rover/data/processed/lab019/lab019_bpsk_receiver.png b/Projects/SDR_Rover/data/processed/lab019/lab019_bpsk_receiver.png new file mode 100644 index 0000000..1b7c7e6 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab019/lab019_bpsk_receiver.png differ diff --git a/Projects/SDR_Rover/data/processed/lab019/lab019_receiver_report.txt b/Projects/SDR_Rover/data/processed/lab019/lab019_receiver_report.txt new file mode 100644 index 0000000..d6100ef --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab019/lab019_receiver_report.txt @@ -0,0 +1,14 @@ +Lab019. BPSK receiver + +Input IQ: data\processed\lab018\lab018_bpsk_tx_iq.npy +Artificial sample delay: 11 +Artificial phase offset: 37.00 deg +Detected sample phase: 11 +Detected frame start symbol: 26 +Correlation score: 0.999937 +Estimated phase: 37.00 deg +Marker bit errors: 0 +Radio sync: 0xD391 +Protocol packet length: 28 bytes +Sequence number: 18 +Restored message: ПРИВЕТ SDR \ No newline at end of file diff --git a/Projects/SDR_Rover/data/processed/lab019/lab019_recovered_packet.bin b/Projects/SDR_Rover/data/processed/lab019/lab019_recovered_packet.bin new file mode 100644 index 0000000..9305e28 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab019/lab019_recovered_packet.bin differ diff --git a/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_rate.png b/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_rate.png new file mode 100644 index 0000000..e038ec7 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_rate.png differ diff --git a/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_report.txt b/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_report.txt new file mode 100644 index 0000000..c770fb0 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_report.txt @@ -0,0 +1,68 @@ +Lab020. Full BPSK frame error rate + +Trials per Eb/N0: 100 +Detection threshold: 0.550 +Frame bit count: 320 + +Eb/N0: 4.0 dB + Success: 3 + Detection fail: 0 + Header error: 34 + CRC error: 38 + Packet error: 25 + Experimental FER: 0.970000 + Ideal FER: 0.982145 + +Eb/N0: 5.0 dB + Success: 24 + Detection fail: 0 + Header error: 11 + CRC error: 41 + Packet error: 24 + Experimental FER: 0.760000 + Ideal FER: 0.852058 + +Eb/N0: 6.0 dB + Success: 58 + Detection fail: 0 + Header error: 5 + CRC error: 29 + Packet error: 8 + Experimental FER: 0.420000 + Ideal FER: 0.534744 + +Eb/N0: 7.0 dB + Success: 84 + Detection fail: 0 + Header error: 1 + CRC error: 12 + Packet error: 3 + Experimental FER: 0.160000 + Ideal FER: 0.219134 + +Eb/N0: 8.0 dB + Success: 98 + Detection fail: 0 + Header error: 0 + CRC error: 2 + Packet error: 0 + Experimental FER: 0.020000 + Ideal FER: 0.059267 + +Eb/N0: 10.0 dB + Success: 100 + Detection fail: 0 + Header error: 0 + CRC error: 0 + Packet error: 0 + Experimental FER: 0.000000 + Ideal FER: 0.001238 + +Eb/N0: 12.0 dB + Success: 100 + Detection fail: 0 + Header error: 0 + CRC error: 0 + Packet error: 0 + Experimental FER: 0.000000 + Ideal FER: 0.000003 diff --git a/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_results.csv b/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_results.csv new file mode 100644 index 0000000..6293a0f --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab020/lab020_frame_error_results.csv @@ -0,0 +1,8 @@ +eb_n0_db,trials,success_count,detection_fail_count,header_error_count,crc_error_count,packet_error_count,experimental_fer,theoretical_ber,ideal_fer,mean_correlation,mean_marker_bit_errors +4.0,100,3,0,34,38,25,0.97,0.012500818040737556,0.98214506597389,0.8537512081010437,0.94 +5.0,100,24,0,11,41,24,0.76,0.0059538671477786615,0.8520575402615456,0.8753653434073579,0.44 +6.0,100,58,0,5,29,8,0.42,0.002388290780932807,0.534743836041445,0.8986643099612688,0.15 +7.0,100,84,0,1,12,3,0.16,0.0007726748153784444,0.21913384039715306,0.9164792561326658,0.08 +8.0,100,98,0,0,2,0,0.02,0.00019090777407599314,0.05926737573898814,0.9311581513763438,0.01 +10.0,100,100,0,0,0,0,0.0,3.872108215522038e-06,0.001238309688911671,0.9560438959972798,0.0 +12.0,100,100,0,0,0,0,0.0,9.006010350628754e-09,2.88191917244148e-06,0.9716348856405669,0.0 diff --git a/Projects/SDR_Rover/data/processed/lab021/lab021_frequency_offset.png b/Projects/SDR_Rover/data/processed/lab021/lab021_frequency_offset.png new file mode 100644 index 0000000..55839eb Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab021/lab021_frequency_offset.png differ diff --git a/Projects/SDR_Rover/data/processed/lab021/lab021_frequency_offset_report.txt b/Projects/SDR_Rover/data/processed/lab021/lab021_frequency_offset_report.txt new file mode 100644 index 0000000..2f1f9a9 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab021/lab021_frequency_offset_report.txt @@ -0,0 +1,62 @@ +Lab021. Frequency offset correction + +Symbol rate: 20000 +Sample rate: 640000 +Phase offset: 37.00 deg +Sample delay: 11 + +True frequency offset: 0.00 Hz +Estimated frequency offset: 0.0000 Hz +Frequency estimation error: 0.0000 Hz +Correlation score: 0.999937 +Status without correction: SUCCESS +Status with correction: SUCCESS + +True frequency offset: 10.00 Hz +Estimated frequency offset: 9.9963 Hz +Frequency estimation error: -0.0037 Hz +Correlation score: 0.999937 +Status without correction: SUCCESS +Status with correction: SUCCESS + +True frequency offset: 25.00 Hz +Estimated frequency offset: 24.9907 Hz +Frequency estimation error: -0.0093 Hz +Correlation score: 0.999937 +Status without correction: CRC ERROR +Status with correction: SUCCESS + +True frequency offset: 50.00 Hz +Estimated frequency offset: 49.9815 Hz +Frequency estimation error: -0.0185 Hz +Correlation score: 0.999937 +Status without correction: PACKET ERROR +Status with correction: SUCCESS + +True frequency offset: 100.00 Hz +Estimated frequency offset: 99.9630 Hz +Frequency estimation error: -0.0370 Hz +Correlation score: 0.999938 +Status without correction: HEADER ERROR +Status with correction: SUCCESS + +True frequency offset: 250.00 Hz +Estimated frequency offset: 249.9091 Hz +Frequency estimation error: -0.0909 Hz +Correlation score: 0.999943 +Status without correction: PACKET ERROR +Status with correction: SUCCESS + +True frequency offset: 500.00 Hz +Estimated frequency offset: 499.8285 Hz +Frequency estimation error: -0.1715 Hz +Correlation score: 0.999957 +Status without correction: HEADER ERROR +Status with correction: SUCCESS + +True frequency offset: 1000.00 Hz +Estimated frequency offset: 999.7270 Hz +Frequency estimation error: -0.2730 Hz +Correlation score: 0.999965 +Status without correction: HEADER ERROR +Status with correction: SUCCESS diff --git a/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo.png b/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo.png new file mode 100644 index 0000000..38d4dae Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo.png differ diff --git a/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo_report.txt b/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo_report.txt new file mode 100644 index 0000000..5a96f0d --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo_report.txt @@ -0,0 +1,100 @@ +Lab022. AWGN and carrier frequency offset + +Trials per point: 30 +Detection threshold: 0.450 + +Eb/N0: 6.0 dB +Maximum absolute CFO: 0.0 Hz +Success without CFO correction: 12 +Success with CFO correction: 10 +FER without correction: 0.600000 +FER with correction: 0.666667 +CFO RMSE: 4.642 Hz + +Eb/N0: 6.0 dB +Maximum absolute CFO: 250.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 7 +FER without correction: 1.000000 +FER with correction: 0.766667 +CFO RMSE: 8.605 Hz + +Eb/N0: 6.0 dB +Maximum absolute CFO: 1000.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 10 +FER without correction: 1.000000 +FER with correction: 0.666667 +CFO RMSE: 5.834 Hz + +Eb/N0: 7.0 dB +Maximum absolute CFO: 0.0 Hz +Success without CFO correction: 22 +Success with CFO correction: 19 +FER without correction: 0.266667 +FER with correction: 0.366667 +CFO RMSE: 6.288 Hz + +Eb/N0: 7.0 dB +Maximum absolute CFO: 250.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 16 +FER without correction: 1.000000 +FER with correction: 0.466667 +CFO RMSE: 5.920 Hz + +Eb/N0: 7.0 dB +Maximum absolute CFO: 1000.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 15 +FER without correction: 1.000000 +FER with correction: 0.500000 +CFO RMSE: 6.259 Hz + +Eb/N0: 8.0 dB +Maximum absolute CFO: 0.0 Hz +Success without CFO correction: 29 +Success with CFO correction: 28 +FER without correction: 0.033333 +FER with correction: 0.066667 +CFO RMSE: 4.337 Hz + +Eb/N0: 8.0 dB +Maximum absolute CFO: 250.0 Hz +Success without CFO correction: 3 +Success with CFO correction: 25 +FER without correction: 0.900000 +FER with correction: 0.166667 +CFO RMSE: 3.800 Hz + +Eb/N0: 8.0 dB +Maximum absolute CFO: 1000.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 27 +FER without correction: 1.000000 +FER with correction: 0.100000 +CFO RMSE: 4.464 Hz + +Eb/N0: 10.0 dB +Maximum absolute CFO: 0.0 Hz +Success without CFO correction: 30 +Success with CFO correction: 30 +FER without correction: 0.000000 +FER with correction: 0.000000 +CFO RMSE: 3.679 Hz + +Eb/N0: 10.0 dB +Maximum absolute CFO: 250.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 30 +FER without correction: 1.000000 +FER with correction: 0.000000 +CFO RMSE: 3.879 Hz + +Eb/N0: 10.0 dB +Maximum absolute CFO: 1000.0 Hz +Success without CFO correction: 0 +Success with CFO correction: 29 +FER without correction: 1.000000 +FER with correction: 0.033333 +CFO RMSE: 3.882 Hz diff --git a/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo_results.csv b/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo_results.csv new file mode 100644 index 0000000..0d05cd7 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo_results.csv @@ -0,0 +1,13 @@ +eb_n0_db,max_abs_cfo_hz,trials,success_without_cfo,success_with_cfo,detection_fail_count,fer_without_cfo,fer_with_cfo,mean_correlation,mean_abs_frequency_error_hz,frequency_rmse_hz +6.0,0.0,30,12,10,0,0.6,0.6666666666666667,0.876037158456595,3.9418970310617256,4.6418505363579925 +6.0,250.0,30,0,7,0,1.0,0.7666666666666666,0.855520150369621,6.514990457450565,8.605182044644604 +6.0,1000.0,30,0,10,0,1.0,0.6666666666666667,0.8536588011138438,4.622721985654384,5.833544274473631 +7.0,0.0,30,22,19,0,0.2666666666666667,0.3666666666666667,0.8904198516865948,5.0932685092625904,6.288037182687809 +7.0,250.0,30,0,16,0,1.0,0.4666666666666667,0.9051298885277537,4.734329281741164,5.920393525914459 +7.0,1000.0,30,0,15,0,1.0,0.5,0.8993182923641181,4.926709522385838,6.258581300752829 +8.0,0.0,30,29,28,0,0.033333333333333326,0.06666666666666665,0.9242694247736734,3.4370575935466303,4.336980161418882 +8.0,250.0,30,3,25,0,0.9,0.16666666666666663,0.9295977117848586,3.0564854854589365,3.8003732773040912 +8.0,1000.0,30,0,27,0,1.0,0.09999999999999998,0.9299247907073535,3.630759939907249,4.464491877765011 +10.0,0.0,30,30,30,0,0.0,0.0,0.9540049007144441,2.953454502291149,3.678772529679632 +10.0,250.0,30,0,30,0,1.0,0.0,0.9544231119966623,3.134578534251591,3.8788146229760336 +10.0,1000.0,30,0,29,0,1.0,0.033333333333333326,0.9530487035097676,3.0232518700094944,3.8817678666309656 diff --git a/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo.png b/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo.png new file mode 100644 index 0000000..4651f75 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo.png differ diff --git a/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo_report.txt b/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo_report.txt new file mode 100644 index 0000000..8af2ec0 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo_report.txt @@ -0,0 +1,101 @@ +Lab023. Guarded CFO correction + +Trials per point: 200 +CFO dead zone: 15.00 Hz +Minimum phase consistency: 0.550 + +Eb/N0: 6.0 dB +Maximum absolute CFO: 0.0 Hz +FER phase only: 0.460000 +FER always CFO: 0.575000 +FER guarded CFO: 0.460000 +Guard apply rate: 0.000000 +CFO RMSE: 5.203 Hz + +Eb/N0: 6.0 dB +Maximum absolute CFO: 250.0 Hz +FER phase only: 0.985000 +FER always CFO: 0.610000 +FER guarded CFO: 0.625000 +Guard apply rate: 0.925000 +CFO RMSE: 5.448 Hz + +Eb/N0: 6.0 dB +Maximum absolute CFO: 1000.0 Hz +FER phase only: 0.995000 +FER always CFO: 0.585000 +FER guarded CFO: 0.585000 +Guard apply rate: 0.990000 +CFO RMSE: 8.473 Hz + +Eb/N0: 7.0 dB +Maximum absolute CFO: 0.0 Hz +FER phase only: 0.200000 +FER always CFO: 0.320000 +FER guarded CFO: 0.200000 +Guard apply rate: 0.000000 +CFO RMSE: 4.745 Hz + +Eb/N0: 7.0 dB +Maximum absolute CFO: 250.0 Hz +FER phase only: 0.955000 +FER always CFO: 0.275000 +FER guarded CFO: 0.335000 +Guard apply rate: 0.855000 +CFO RMSE: 4.950 Hz + +Eb/N0: 7.0 dB +Maximum absolute CFO: 1000.0 Hz +FER phase only: 0.990000 +FER always CFO: 0.275000 +FER guarded CFO: 0.280000 +Guard apply rate: 0.975000 +CFO RMSE: 5.127 Hz + +Eb/N0: 8.0 dB +Maximum absolute CFO: 0.0 Hz +FER phase only: 0.040000 +FER always CFO: 0.110000 +FER guarded CFO: 0.040000 +Guard apply rate: 0.000000 +CFO RMSE: 4.277 Hz + +Eb/N0: 8.0 dB +Maximum absolute CFO: 250.0 Hz +FER phase only: 0.970000 +FER always CFO: 0.125000 +FER guarded CFO: 0.205000 +Guard apply rate: 0.880000 +CFO RMSE: 4.534 Hz + +Eb/N0: 8.0 dB +Maximum absolute CFO: 1000.0 Hz +FER phase only: 0.995000 +FER always CFO: 0.105000 +FER guarded CFO: 0.110000 +Guard apply rate: 0.990000 +CFO RMSE: 4.749 Hz + +Eb/N0: 10.0 dB +Maximum absolute CFO: 0.0 Hz +FER phase only: 0.000000 +FER always CFO: 0.000000 +FER guarded CFO: 0.000000 +Guard apply rate: 0.000000 +CFO RMSE: 3.345 Hz + +Eb/N0: 10.0 dB +Maximum absolute CFO: 250.0 Hz +FER phase only: 0.960000 +FER always CFO: 0.000000 +FER guarded CFO: 0.045000 +Guard apply rate: 0.915000 +CFO RMSE: 3.305 Hz + +Eb/N0: 10.0 dB +Maximum absolute CFO: 1000.0 Hz +FER phase only: 0.990000 +FER always CFO: 0.000000 +FER guarded CFO: 0.005000 +Guard apply rate: 0.985000 +CFO RMSE: 3.508 Hz diff --git a/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo_results.csv b/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo_results.csv new file mode 100644 index 0000000..738821c --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo_results.csv @@ -0,0 +1,13 @@ +eb_n0_db,max_abs_cfo_hz,trials,success_phase_only,success_always_cfo,success_guarded_cfo,fer_phase_only,fer_always_cfo,fer_guarded_cfo,guarded_apply_count,guarded_apply_rate,frequency_rmse_hz,mean_phase_consistency +6.0,0.0,200,108,85,108,0.45999999999999996,0.575,0.45999999999999996,0,0.0,5.202816842065136,0.8803685059021725 +6.0,250.0,200,3,78,75,0.985,0.61,0.625,185,0.925,5.448336211547444,0.8820273978803879 +6.0,1000.0,200,1,83,83,0.995,0.585,0.585,198,0.99,8.473194122480628,0.8815296088302559 +7.0,0.0,200,160,136,160,0.19999999999999996,0.31999999999999995,0.19999999999999996,0,0.0,4.744964108330319,0.9044304298907866 +7.0,250.0,200,9,145,133,0.955,0.275,0.33499999999999996,171,0.855,4.949784071638471,0.9048829617513402 +7.0,1000.0,200,2,145,144,0.99,0.275,0.28,195,0.975,5.126941255642888,0.9037603894844779 +8.0,0.0,200,192,178,192,0.040000000000000036,0.10999999999999999,0.040000000000000036,0,0.0,4.277056721707394,0.9222860828489162 +8.0,250.0,200,6,175,159,0.97,0.125,0.20499999999999996,176,0.88,4.534455139069744,0.9233618150972334 +8.0,1000.0,200,1,179,178,0.995,0.10499999999999998,0.10999999999999999,198,0.99,4.749146564125825,0.9260637045844348 +10.0,0.0,200,200,200,200,0.0,0.0,0.0,0,0.0,3.3448810933215865,0.9511723197247827 +10.0,250.0,200,8,200,191,0.96,0.0,0.04500000000000004,183,0.915,3.3052349602075664,0.9514399539276094 +10.0,1000.0,200,2,200,199,0.99,0.0,0.0050000000000000044,197,0.985,3.5081991053844495,0.9505037355131278 diff --git a/Projects/SDR_Rover/data/processed/lab024b/lab024b_fm_candidates.csv b/Projects/SDR_Rover/data/processed/lab024b/lab024b_fm_candidates.csv new file mode 100644 index 0000000..15afd43 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab024b/lab024b_fm_candidates.csv @@ -0,0 +1,8 @@ +frequency_hz,frequency_mhz,relative_power_db,prominence_db +98840137.685,98.840138,-16.363,14.939 +99201367.853,99.201368,-8.578,21.049 +99603613.612,99.603614,-5.852,194.148 +100099316.323,100.099316,-0.641,27.938 +100498925.365,100.498925,0.000,200.000 +100896483.628,100.896484,-2.662,25.928 +101159569.346,101.159569,-11.137,18.231 diff --git a/Projects/SDR_Rover/data/processed/lab024b/lab024b_report.txt b/Projects/SDR_Rover/data/processed/lab024b/lab024b_report.txt new file mode 100644 index 0000000..8278760 --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab024b/lab024b_report.txt @@ -0,0 +1,19 @@ +Lab024B. Усреднённый спектр Pluto+ методом Уэлча + +URI: ip:192.168.2.1 +Центральная частота: 100.000 МГц +Частота дискретизации: 2.400 Мвыб/с +Полоса RX: 2.000 МГц +Количество IQ-сэмплов: 1048576 +Размер сегмента Уэлча: 8192 +Перекрытие сегментов: 4096 + +Найдено кандидатов: 7 + +01. 98.840138 МГц, уровень -16.36 дБ, выраженность 14.94 дБ +02. 99.201368 МГц, уровень -8.58 дБ, выраженность 21.05 дБ +03. 99.603614 МГц, уровень -5.85 дБ, выраженность 194.15 дБ +04. 100.099316 МГц, уровень -0.64 дБ, выраженность 27.94 дБ +05. 100.498925 МГц, уровень 0.00 дБ, выраженность 200.00 дБ +06. 100.896484 МГц, уровень -2.66 дБ, выраженность 25.93 дБ +07. 101.159569 МГц, уровень -11.14 дБ, выраженность 18.23 дБ \ No newline at end of file diff --git a/Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.csv b/Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.csv new file mode 100644 index 0000000..92777ff --- /dev/null +++ b/Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.csv @@ -0,0 +1,8193 @@ +frequency_hz,frequency_mhz,raw_relative_power_db,smoothed_relative_power_db +98800001.000,98.800001,-10.726385,-14.649957 +98800293.969,98.800294,-10.547041,-14.607644 +98800586.937,98.800587,-10.745271,-14.563702 +98800879.906,98.800880,-10.525955,-14.516100 +98801172.874,98.801173,-10.732759,-14.466744 +98801465.843,98.801466,-11.182201,-14.418696 +98801758.811,98.801759,-11.009547,-14.373212 +98802051.780,98.802052,-11.137178,-14.336434 +98802344.748,98.802345,-11.514388,-14.298604 +98802637.717,98.802638,-11.133512,-14.260213 +98802930.685,98.802931,-11.009096,-14.222369 +98803223.654,98.803224,-11.043127,-14.184620 +98803516.622,98.803517,-10.501867,-14.151324 +98803809.591,98.803810,-10.965973,-14.118623 +98804102.559,98.804103,-11.003119,-14.085221 +98804395.528,98.804396,-11.604969,-14.050387 +98804688.496,98.804688,-11.751930,-14.017580 +98804981.465,98.804981,-10.932662,-13.981537 +98805274.433,98.805274,-11.426690,-13.947024 +98805567.402,98.805567,-11.493180,-13.915573 +98805860.370,98.805860,-11.539065,-13.882647 +98806153.339,98.806153,-11.948833,-13.851221 +98806446.307,98.806446,-12.072056,-13.823063 +98806739.276,98.806739,-11.850609,-13.795888 +98807032.244,98.807032,-11.557620,-13.769898 +98807325.213,98.807325,-11.368958,-13.743158 +98807618.181,98.807618,-11.610835,-13.714981 +98807911.150,98.807911,-11.619913,-13.688183 +98808204.118,98.808204,-11.914223,-13.661340 +98808497.087,98.808497,-11.533113,-13.632441 +98808790.055,98.808790,-11.840443,-13.602083 +98809083.024,98.809083,-11.597950,-13.574239 +98809375.992,98.809376,-11.590609,-13.552367 +98809668.961,98.809669,-12.028018,-13.532203 +98809961.929,98.809962,-11.657974,-13.509748 +98810254.898,98.810255,-11.649085,-13.485522 +98810547.866,98.810548,-12.023540,-13.464725 +98810840.835,98.810841,-11.912182,-13.444528 +98811133.803,98.811134,-11.726668,-13.423417 +98811426.772,98.811427,-12.237082,-13.402970 +98811719.740,98.811720,-11.966451,-13.377958 +98812012.709,98.812013,-11.806126,-13.352410 +98812305.677,98.812306,-11.465884,-13.332358 +98812598.646,98.812599,-11.794741,-13.312694 +98812891.614,98.812892,-11.703217,-13.293459 +98813184.583,98.813185,-11.292375,-13.275652 +98813477.551,98.813478,-11.233776,-13.257483 +98813770.520,98.813771,-11.920930,-13.240453 +98814063.488,98.814063,-11.902260,-13.221731 +98814356.457,98.814356,-11.780872,-13.202907 +98814649.425,98.814649,-12.156719,-13.184381 +98814942.394,98.814942,-12.450939,-13.164603 +98815235.362,98.815235,-12.293976,-13.143926 +98815528.331,98.815528,-12.443504,-13.123524 +98815821.299,98.815821,-12.178456,-13.106348 +98816114.268,98.816114,-12.282426,-13.089970 +98816407.236,98.816407,-12.135432,-13.073363 +98816700.205,98.816700,-12.403315,-13.057004 +98816993.173,98.816993,-12.020175,-13.040973 +98817286.142,98.817286,-12.166582,-13.024496 +98817579.110,98.817579,-12.033157,-13.011288 +98817872.079,98.817872,-12.142842,-12.997233 +98818165.047,98.818165,-12.447142,-12.981457 +98818458.016,98.818458,-12.273903,-12.966152 +98818750.984,98.818751,-12.125439,-12.951113 +98819043.953,98.819044,-12.577471,-12.935320 +98819336.921,98.819337,-12.669459,-12.921715 +98819629.890,98.819630,-12.723595,-12.909138 +98819922.858,98.819923,-13.262470,-12.898295 +98820215.827,98.820216,-13.374613,-12.939551 +98820508.795,98.820509,-13.167578,-12.984697 +98820801.764,98.820802,-12.774193,-13.024816 +98821094.732,98.821095,-12.568709,-13.067658 +98821387.701,98.821388,-12.636545,-13.110784 +98821680.669,98.821681,-12.828000,-13.147364 +98821973.638,98.821974,-13.709509,-13.185599 +98822266.606,98.822267,-13.549784,-13.224697 +98822559.575,98.822560,-13.447778,-13.260043 +98822852.543,98.822853,-13.471944,-13.300832 +98823145.512,98.823146,-13.445103,-13.343243 +98823438.480,98.823438,-13.954618,-13.385281 +98823731.449,98.823731,-13.999941,-13.436045 +98824024.417,98.824024,-13.874878,-13.482029 +98824317.386,98.824317,-13.658344,-13.526855 +98824610.354,98.824610,-13.884942,-13.564648 +98824903.323,98.824903,-13.442075,-13.602093 +98825196.292,98.825196,-13.595050,-13.648960 +98825489.260,98.825489,-13.965585,-13.690263 +98825782.229,98.825782,-13.734262,-13.732261 +98826075.197,98.826075,-13.904757,-13.774941 +98826368.166,98.826368,-14.351778,-13.813667 +98826661.134,98.826661,-14.478289,-13.850587 +98826954.103,98.826954,-14.645511,-13.891327 +98827247.071,98.827247,-14.495500,-13.936960 +98827540.040,98.827540,-14.240859,-13.983899 +98827833.008,98.827833,-14.431153,-14.027477 +98828125.977,98.828126,-14.397080,-14.071515 +98828418.945,98.828419,-14.048723,-14.112695 +98828711.914,98.828712,-13.805101,-14.159922 +98829004.882,98.829005,-14.151508,-14.204299 +98829297.851,98.829298,-15.175119,-14.253903 +98829590.819,98.829591,-15.507086,-14.304403 +98829883.788,98.829884,-15.018517,-14.349749 +98830176.756,98.830177,-14.665362,-14.400854 +98830469.725,98.830470,-15.305876,-14.452462 +98830762.693,98.830763,-15.412257,-14.499191 +98831055.662,98.831056,-15.199495,-14.547414 +98831348.630,98.831349,-15.317513,-14.600476 +98831641.599,98.831642,-14.419737,-14.646809 +98831934.567,98.831935,-14.302124,-14.697211 +98832227.536,98.832228,-15.331476,-14.749625 +98832520.504,98.832521,-15.396440,-14.808816 +98832813.473,98.832813,-15.472933,-14.864652 +98833106.441,98.833106,-15.789077,-14.922345 +98833399.410,98.833399,-15.684046,-14.986994 +98833692.378,98.833692,-15.947403,-15.054154 +98833985.347,98.833985,-15.518284,-15.111168 +98834278.315,98.834278,-15.475703,-15.168775 +98834571.284,98.834571,-15.526472,-15.229544 +98834864.252,98.834864,-15.223415,-15.285631 +98835157.221,98.835157,-15.009887,-15.337455 +98835450.189,98.835450,-15.047626,-15.392470 +98835743.158,98.835743,-15.776361,-15.445756 +98836036.126,98.836036,-15.966335,-15.502984 +98836329.095,98.836329,-15.889359,-15.558355 +98836622.063,98.836622,-15.938179,-15.616089 +98836915.032,98.836915,-16.010015,-15.672258 +98837208.000,98.837208,-15.874731,-15.737851 +98837500.969,98.837501,-16.820099,-15.801379 +98837793.937,98.837794,-16.536358,-15.866853 +98838086.906,98.838087,-16.020212,-15.931065 +98838379.874,98.838380,-16.136089,-15.991914 +98838672.843,98.838673,-16.197140,-16.056852 +98838965.811,98.838966,-15.969003,-16.125039 +98839258.780,98.839259,-16.602417,-16.187334 +98839551.748,98.839552,-16.929989,-16.248377 +98839844.717,98.839845,-17.563023,-16.307947 +98840137.685,98.840138,-17.412981,-16.362796 +98840430.654,98.840431,-17.914919,-16.417191 +98840723.622,98.840724,-16.804443,-16.475684 +98841016.591,98.841017,-16.660116,-16.542096 +98841309.559,98.841310,-17.525620,-16.614054 +98841602.528,98.841603,-16.983414,-16.685110 +98841895.496,98.841895,-16.761030,-16.753172 +98842188.465,98.842188,-17.447294,-16.808309 +98842481.433,98.842481,-17.517298,-16.868249 +98842774.402,98.842774,-17.806190,-16.930920 +98843067.370,98.843067,-17.694273,-16.994626 +98843360.339,98.843360,-17.561556,-17.060033 +98843653.307,98.843653,-17.940083,-17.118601 +98843946.276,98.843946,-18.342083,-17.176723 +98844239.244,98.844239,-17.884398,-17.238506 +98844532.213,98.844532,-17.849361,-17.304809 +98844825.181,98.844825,-18.226070,-17.368244 +98845118.150,98.845118,-17.821253,-17.440647 +98845411.118,98.845411,-17.950708,-17.511365 +98845704.087,98.845704,-18.375126,-17.577051 +98845997.055,98.845997,-18.718842,-17.648199 +98846290.024,98.846290,-18.910702,-17.718049 +98846582.992,98.846583,-18.581072,-17.781907 +98846875.961,98.846876,-18.997135,-17.845028 +98847168.929,98.847169,-19.427320,-17.905872 +98847461.898,98.847462,-18.704739,-17.971072 +98847754.866,98.847755,-18.430161,-18.042169 +98848047.835,98.848048,-18.480433,-18.111062 +98848340.803,98.848341,-18.625463,-18.181564 +98848633.772,98.848634,-18.935095,-18.258396 +98848926.740,98.848927,-19.202774,-18.342578 +98849219.709,98.849220,-19.947369,-18.421552 +98849512.677,98.849513,-20.060980,-18.484339 +98849805.646,98.849806,-20.055510,-18.542253 +98850098.615,98.850099,-20.269430,-18.609158 +98850391.583,98.850392,-20.150742,-18.683445 +98850684.552,98.850685,-19.931456,-18.748743 +98850977.520,98.850978,-19.706663,-18.813789 +98851270.489,98.851270,-20.552111,-18.882774 +98851563.457,98.851563,-20.296646,-18.951034 +98851856.426,98.851856,-20.286779,-19.038167 +98852149.394,98.852149,-19.891836,-19.129559 +98852442.363,98.852442,-20.349169,-19.202163 +98852735.331,98.852735,-20.800915,-19.274795 +98853028.300,98.853028,-20.666679,-19.347527 +98853321.268,98.853321,-20.412827,-19.416247 +98853614.237,98.853614,-20.660114,-19.488063 +98853907.205,98.853907,-20.730291,-19.556391 +98854200.174,98.854200,-20.511869,-19.633625 +98854493.142,98.854493,-20.727404,-19.713311 +98854786.111,98.854786,-20.898798,-19.793142 +98855079.079,98.855079,-20.577845,-19.881038 +98855372.048,98.855372,-20.701342,-19.975642 +98855665.016,98.855665,-20.602545,-20.071173 +98855957.985,98.855958,-20.280581,-20.153214 +98856250.953,98.856251,-19.896666,-20.233081 +98856543.922,98.856544,-19.646101,-20.316279 +98856836.890,98.856837,-20.365700,-20.399708 +98857129.859,98.857130,-21.441669,-20.483227 +98857422.827,98.857423,-21.169216,-20.571693 +98857715.796,98.857716,-20.597225,-20.643436 +98858008.764,98.858009,-20.469650,-20.721412 +98858301.733,98.858302,-20.821325,-20.811392 +98858594.701,98.858595,-20.930748,-20.900735 +98858887.670,98.858888,-20.763247,-20.990762 +98859180.638,98.859181,-21.179438,-21.088606 +98859473.607,98.859474,-20.926619,-21.174817 +98859766.575,98.859767,-20.391523,-21.255797 +98860059.544,98.860060,-21.581509,-21.326013 +98860352.512,98.860353,-21.820120,-21.400384 +98860645.481,98.860645,-21.938086,-21.467639 +98860938.449,98.860938,-22.227064,-21.556633 +98861231.418,98.861231,-22.645681,-21.651067 +98861524.386,98.861524,-22.210297,-21.729794 +98861817.355,98.861817,-21.909763,-21.821135 +98862110.323,98.862110,-22.172817,-21.919647 +98862403.292,98.862403,-23.009808,-22.004965 +98862696.260,98.862696,-23.152040,-22.090470 +98862989.229,98.862989,-23.453215,-22.171655 +98863282.197,98.863282,-23.638968,-22.256666 +98863575.166,98.863575,-23.855562,-22.346680 +98863868.134,98.863868,-23.515190,-22.430658 +98864161.103,98.864161,-24.086962,-22.507729 +98864454.071,98.864454,-24.126497,-22.595950 +98864747.040,98.864747,-24.056496,-22.686584 +98865040.008,98.865040,-24.261417,-22.770851 +98865332.977,98.865333,-24.190161,-22.865824 +98865625.945,98.865626,-24.373497,-22.960216 +98865918.914,98.865919,-24.644026,-23.047547 +98866211.882,98.866212,-25.048117,-23.128815 +98866504.851,98.866505,-25.384249,-23.207775 +98866797.819,98.866798,-25.660782,-23.295322 +98867090.788,98.867091,-25.218603,-23.376373 +98867383.756,98.867384,-26.098932,-23.450633 +98867676.725,98.867677,-26.633314,-23.540991 +98867969.693,98.867970,-26.563559,-23.639570 +98868262.662,98.868263,-26.475607,-23.738840 +98868555.630,98.868556,-25.450609,-23.836590 +98868848.599,98.868849,-26.265371,-23.930261 +98869141.567,98.869142,-26.408243,-24.019528 +98869434.536,98.869435,-26.797209,-24.094612 +98869727.504,98.869728,-26.178358,-24.168847 +98870020.473,98.870020,-26.593744,-24.245551 +98870313.441,98.870313,-26.675669,-24.319487 +98870606.410,98.870606,-27.475420,-24.397365 +98870899.378,98.870899,-27.979208,-24.481639 +98871192.347,98.871192,-27.369568,-24.571741 +98871485.315,98.871485,-27.542830,-24.645917 +98871778.284,98.871778,-28.250246,-24.726569 +98872071.252,98.872071,-28.141836,-24.808536 +98872364.221,98.872364,-28.109283,-24.900112 +98872657.189,98.872657,-28.067041,-24.983933 +98872950.158,98.872950,-28.319483,-25.061673 +98873243.126,98.873243,-28.617058,-25.143343 +98873536.095,98.873536,-28.816145,-25.231249 +98873829.063,98.873829,-28.652830,-25.315413 +98874122.032,98.874122,-28.794321,-25.398687 +98874415.000,98.874415,-29.060440,-25.488305 +98874707.969,98.874708,-28.674257,-25.575177 +98875000.938,98.875001,-29.032713,-25.660619 +98875293.906,98.875294,-29.169922,-25.754253 +98875586.875,98.875587,-28.849186,-25.846881 +98875879.843,98.875880,-29.115900,-25.944585 +98876172.812,98.876173,-29.151686,-26.053655 +98876465.780,98.876466,-29.469492,-26.176686 +98876758.749,98.876759,-29.055597,-26.310561 +98877051.717,98.877052,-28.993666,-26.426833 +98877344.686,98.877345,-29.437954,-26.518243 +98877637.654,98.877638,-29.510288,-26.618070 +98877930.623,98.877931,-29.284464,-26.735412 +98878223.591,98.878224,-29.174549,-26.860771 +98878516.560,98.878517,-29.200806,-26.979230 +98878809.528,98.878810,-29.348867,-27.096942 +98879102.497,98.879102,-29.982590,-27.222190 +98879395.465,98.879395,-30.418325,-27.337956 +98879688.434,98.879688,-30.147501,-27.465751 +98879981.402,98.879981,-29.697653,-27.616385 +98880274.371,98.880274,-29.990839,-27.731390 +98880567.339,98.880567,-30.397364,-27.842641 +98880860.308,98.880860,-29.778862,-27.953160 +98881153.276,98.881153,-30.064320,-28.057812 +98881446.245,98.881446,-30.702213,-28.153632 +98881739.213,98.881739,-30.537275,-28.264877 +98882032.182,98.882032,-30.559502,-28.387643 +98882325.150,98.882325,-30.604002,-28.506536 +98882618.119,98.882618,-30.560141,-28.605544 +98882911.087,98.882911,-30.488922,-28.704385 +98883204.056,98.883204,-30.466187,-28.794820 +98883497.024,98.883497,-30.871193,-28.881208 +98883789.993,98.883790,-31.051659,-28.965483 +98884082.961,98.884083,-30.559031,-29.060532 +98884375.930,98.884376,-30.904068,-29.143080 +98884668.898,98.884669,-30.733013,-29.226774 +98884961.867,98.884962,-30.638031,-29.311300 +98885254.835,98.885255,-30.666260,-29.392486 +98885547.804,98.885548,-30.946733,-29.476117 +98885840.772,98.885841,-31.294483,-29.556835 +98886133.741,98.886134,-30.753422,-29.634224 +98886426.709,98.886427,-30.749369,-29.705700 +98886719.678,98.886720,-30.961443,-29.772028 +98887012.646,98.887013,-31.326445,-29.832716 +98887305.615,98.887306,-31.336924,-29.904711 +98887598.583,98.887599,-31.365940,-29.958266 +98887891.552,98.887892,-31.253752,-30.002003 +98888184.520,98.888185,-30.983698,-30.047028 +98888477.489,98.888477,-30.798601,-30.096639 +98888770.457,98.888770,-31.581261,-30.168077 +98889063.426,98.889063,-31.312277,-30.222931 +98889356.394,98.889356,-30.885136,-30.273024 +98889649.363,98.889649,-30.882296,-30.318281 +98889942.331,98.889942,-31.619122,-30.372516 +98890235.300,98.890235,-31.561272,-30.416919 +98890528.268,98.890528,-31.849848,-30.466763 +98890821.237,98.890821,-32.261581,-30.504284 +98891114.205,98.891114,-31.703796,-30.535617 +98891407.174,98.891407,-31.491713,-30.576892 +98891700.142,98.891700,-31.603128,-30.618456 +98891993.111,98.891993,-31.339056,-30.646763 +98892286.079,98.892286,-30.941162,-30.674467 +98892579.048,98.892579,-31.229538,-30.706577 +98892872.016,98.892872,-32.208595,-30.738943 +98893164.985,98.893165,-32.076664,-30.766022 +98893457.953,98.893458,-31.586000,-30.787031 +98893750.922,98.893751,-31.511564,-30.805611 +98894043.890,98.894044,-30.964827,-30.829308 +98894336.859,98.894337,-30.883120,-30.854064 +98894629.827,98.894630,-31.021397,-30.873854 +98894922.796,98.894923,-31.322374,-30.895160 +98895215.764,98.895216,-30.874996,-30.913661 +98895508.733,98.895509,-30.840382,-30.932302 +98895801.701,98.895802,-31.110853,-30.953687 +98896094.670,98.896095,-31.367891,-30.970669 +98896387.638,98.896388,-31.267685,-30.989314 +98896680.607,98.896681,-30.865942,-31.004870 +98896973.575,98.896974,-31.373636,-31.023899 +98897266.544,98.897267,-31.683973,-31.044190 +98897559.512,98.897560,-31.552572,-31.062994 +98897852.481,98.897852,-31.199915,-31.079375 +98898145.449,98.898145,-31.493626,-31.093688 +98898438.418,98.898438,-31.661829,-31.113139 +98898731.386,98.898731,-31.387691,-31.132437 +98899024.355,98.899024,-31.028530,-31.147952 +98899317.323,98.899317,-31.022795,-31.159266 +98899610.292,98.899610,-31.274399,-31.165516 +98899903.260,98.899903,-31.152300,-31.171166 +98900196.229,98.900196,-31.252367,-31.183982 +98900489.198,98.900489,-31.339705,-31.197485 +98900782.166,98.900782,-31.289934,-31.204457 +98901075.135,98.901075,-31.177963,-31.214172 +98901368.103,98.901368,-31.178110,-31.223255 +98901661.072,98.901661,-31.481333,-31.227841 +98901954.040,98.901954,-31.259596,-31.233166 +98902247.009,98.902247,-31.545492,-31.238071 +98902539.977,98.902540,-31.902250,-31.241611 +98902832.946,98.902833,-32.295326,-31.246810 +98903125.914,98.903126,-31.484386,-31.254114 +98903418.883,98.903419,-31.163443,-31.260986 +98903711.851,98.903712,-31.492283,-31.264260 +98904004.820,98.904005,-31.625816,-31.267828 +98904297.788,98.904298,-31.550074,-31.273891 +98904590.757,98.904591,-31.665194,-31.277356 +98904883.725,98.904884,-31.110388,-31.280251 +98905176.694,98.905177,-31.105598,-31.284023 +98905469.662,98.905470,-30.940584,-31.286921 +98905762.631,98.905763,-30.932457,-31.289282 +98906055.599,98.906056,-31.233068,-31.287139 +98906348.568,98.906349,-31.582460,-31.291428 +98906641.536,98.906642,-31.723135,-31.295022 +98906934.505,98.906935,-31.472988,-31.294274 +98907227.473,98.907227,-31.763815,-31.292785 +98907520.442,98.907520,-31.313499,-31.289285 +98907813.410,98.907813,-31.051241,-31.287893 +98908106.379,98.908106,-31.002617,-31.288331 +98908399.347,98.908399,-31.475243,-31.294997 +98908692.316,98.908692,-31.773817,-31.301674 +98908985.284,98.908985,-31.494955,-31.300009 +98909278.253,98.909278,-31.001795,-31.299703 +98909571.221,98.909571,-31.209301,-31.301052 +98909864.190,98.909864,-30.805414,-31.295496 +98910157.158,98.910157,-30.400476,-31.283666 +98910450.127,98.910450,-31.368475,-31.281307 +98910743.095,98.910743,-31.367147,-31.276715 +98911036.064,98.911036,-31.453266,-31.268837 +98911329.032,98.911329,-31.630463,-31.268088 +98911622.001,98.911622,-32.088509,-31.266374 +98911914.969,98.911915,-31.401075,-31.260570 +98912207.938,98.912208,-31.060528,-31.261418 +98912500.906,98.912501,-31.665308,-31.265767 +98912793.875,98.912794,-31.573053,-31.265899 +98913086.843,98.913087,-31.225357,-31.258845 +98913379.812,98.913380,-30.857912,-31.250575 +98913672.780,98.913673,-30.833105,-31.248036 +98913965.749,98.913966,-31.281960,-31.247792 +98914258.717,98.914259,-31.696363,-31.254035 +98914551.686,98.914552,-31.367605,-31.255872 +98914844.654,98.914845,-30.921980,-31.259527 +98915137.623,98.915138,-31.103907,-31.260703 +98915430.591,98.915431,-31.336081,-31.266067 +98915723.560,98.915724,-31.189159,-31.269550 +98916016.528,98.916017,-30.988016,-31.269923 +98916309.497,98.916309,-31.270407,-31.268059 +98916602.465,98.916602,-31.308504,-31.265289 +98916895.434,98.916895,-31.148260,-31.265049 +98917188.402,98.917188,-31.213022,-31.259185 +98917481.371,98.917481,-31.721518,-31.250987 +98917774.339,98.917774,-31.453337,-31.243765 +98918067.308,98.918067,-30.822195,-31.238812 +98918360.276,98.918360,-31.346540,-31.234119 +98918653.245,98.918653,-31.355082,-31.227934 +98918946.213,98.918946,-31.049503,-31.221844 +98919239.182,98.919239,-31.364607,-31.219894 +98919532.150,98.919532,-31.207247,-31.215770 +98919825.119,98.919825,-30.807037,-31.210385 +98920118.087,98.920118,-31.169971,-31.202304 +98920411.056,98.920411,-31.685812,-31.197896 +98920704.024,98.920704,-31.274055,-31.199822 +98920996.993,98.920997,-30.860378,-31.195643 +98921289.961,98.921290,-31.141447,-31.190278 +98921582.930,98.921583,-31.298250,-31.186008 +98921875.898,98.921876,-31.208445,-31.182046 +98922168.867,98.922169,-31.176521,-31.174045 +98922461.835,98.922462,-31.044516,-31.165378 +98922754.804,98.922755,-31.215693,-31.160044 +98923047.772,98.923048,-31.422092,-31.151426 +98923340.741,98.923341,-31.331511,-31.148617 +98923633.709,98.923634,-31.301767,-31.147150 +98923926.678,98.923927,-31.543764,-31.146415 +98924219.646,98.924220,-31.328375,-31.143878 +98924512.615,98.924513,-31.363298,-31.140435 +98924805.583,98.924806,-31.097639,-31.135578 +98925098.552,98.925099,-31.108225,-31.135154 +98925391.521,98.925392,-31.024939,-31.137937 +98925684.489,98.925684,-31.256536,-31.138827 +98925977.458,98.925977,-31.009916,-31.139941 +98926270.426,98.926270,-31.306604,-31.134171 +98926563.395,98.926563,-31.207125,-31.124738 +98926856.363,98.926856,-30.867521,-31.115706 +98927149.332,98.927149,-31.125652,-31.107604 +98927442.300,98.927442,-30.877420,-31.096726 +98927735.269,98.927735,-31.176064,-31.095407 +98928028.237,98.928028,-31.313742,-31.095643 +98928321.206,98.928321,-31.930536,-31.092287 +98928614.174,98.928614,-31.701544,-31.083309 +98928907.143,98.928907,-31.344603,-31.069395 +98929200.111,98.929200,-31.270346,-31.063515 +98929493.080,98.929493,-31.056440,-31.066999 +98929786.048,98.929786,-30.240479,-31.066664 +98930079.017,98.930079,-30.149794,-31.048787 +98930371.985,98.930372,-31.229750,-31.043062 +98930664.954,98.930665,-31.185709,-31.042432 +98930957.922,98.930958,-31.082062,-31.037476 +98931250.891,98.931251,-31.591816,-31.034038 +98931543.859,98.931544,-31.251307,-31.028913 +98931836.828,98.931837,-30.818624,-31.021255 +98932129.796,98.932130,-31.458874,-31.019660 +98932422.765,98.932423,-31.532885,-31.019047 +98932715.733,98.932716,-31.247545,-31.013946 +98933008.702,98.933009,-31.147768,-31.006179 +98933301.670,98.933302,-30.886585,-31.001744 +98933594.639,98.933595,-31.225489,-31.001411 +98933887.607,98.933888,-31.476206,-31.002704 +98934180.576,98.934181,-31.850073,-30.998285 +98934473.544,98.934474,-31.120420,-30.991051 +98934766.513,98.934767,-31.523577,-30.987572 +98935059.481,98.935059,-31.489004,-30.988574 +98935352.450,98.935352,-31.605078,-30.988128 +98935645.418,98.935645,-31.295828,-30.982859 +98935938.387,98.935938,-31.160482,-30.977319 +98936231.355,98.936231,-31.114126,-30.977453 +98936524.324,98.936524,-30.903864,-30.975852 +98936817.292,98.936817,-30.836021,-30.972874 +98937110.261,98.937110,-30.618841,-30.968443 +98937403.229,98.937403,-30.593433,-30.965930 +98937696.198,98.937696,-30.602982,-30.960021 +98937989.166,98.937989,-30.574955,-30.953112 +98938282.135,98.938282,-30.860006,-30.951387 +98938575.103,98.938575,-30.814499,-30.946121 +98938868.072,98.938868,-30.597822,-30.944104 +98939161.040,98.939161,-30.780237,-30.943549 +98939454.009,98.939454,-30.513653,-30.940458 +98939746.977,98.939747,-30.584078,-30.937398 +98940039.946,98.940040,-30.177095,-30.939951 +98940332.914,98.940333,-30.680458,-30.940945 +98940625.883,98.940626,-31.621260,-30.939109 +98940918.851,98.940919,-30.741179,-30.942395 +98941211.820,98.941212,-30.501102,-30.945580 +98941504.788,98.941505,-30.630575,-30.943949 +98941797.757,98.941798,-30.935822,-30.940881 +98942090.725,98.942091,-30.265623,-30.936613 +98942383.694,98.942384,-30.412350,-30.934482 +98942676.662,98.942677,-31.112175,-30.933666 +98942969.631,98.942970,-30.981075,-30.929356 +98943262.599,98.943263,-31.087440,-30.925409 +98943555.568,98.943556,-30.966198,-30.923763 +98943848.536,98.943849,-31.384720,-30.919656 +98944141.505,98.944142,-31.253876,-30.912989 +98944434.473,98.944434,-31.060625,-30.910371 +98944727.442,98.944727,-30.972168,-30.909036 +98945020.410,98.945020,-31.053133,-30.905089 +98945313.379,98.945313,-31.501667,-30.899931 +98945606.347,98.945606,-31.058611,-30.900781 +98945899.316,98.945899,-31.080519,-30.898481 +98946192.284,98.946192,-30.491859,-30.894092 +98946485.253,98.946485,-30.343061,-30.888686 +98946778.221,98.946778,-30.493216,-30.885927 +98947071.190,98.947071,-30.408579,-30.886812 +98947364.158,98.947364,-30.303732,-30.888306 +98947657.127,98.947657,-31.127562,-30.886844 +98947950.095,98.947950,-31.083307,-30.882348 +98948243.064,98.948243,-30.574228,-30.878317 +98948536.032,98.948536,-30.303749,-30.871292 +98948829.001,98.948829,-29.968531,-30.861617 +98949121.969,98.949122,-30.686171,-30.852472 +98949414.938,98.949415,-31.499523,-30.847119 +98949707.906,98.949708,-31.162268,-30.846226 +98950000.875,98.950001,-28.952463,-30.854997 +98950293.844,98.950294,-29.772205,-30.858004 +98950586.812,98.950587,-31.276369,-30.849897 +98950879.781,98.950880,-30.690634,-30.843016 +98951172.749,98.951173,-30.963482,-30.837593 +98951465.718,98.951466,-30.891079,-30.828280 +98951758.686,98.951759,-30.920527,-30.823189 +98952051.655,98.952052,-31.168846,-30.822338 +98952344.623,98.952345,-30.976557,-30.816041 +98952637.592,98.952638,-30.921335,-30.807340 +98952930.560,98.952931,-30.504622,-30.806165 +98953223.529,98.953224,-30.628874,-30.806861 +98953516.497,98.953516,-30.813946,-30.807292 +98953809.466,98.953809,-31.007029,-30.803318 +98954102.434,98.954102,-30.679688,-30.801219 +98954395.403,98.954395,-30.664415,-30.796316 +98954688.371,98.954688,-30.876539,-30.795655 +98954981.340,98.954981,-31.059351,-30.790068 +98955274.308,98.955274,-31.041563,-30.782835 +98955567.277,98.955567,-30.616562,-30.776857 +98955860.245,98.955860,-30.457884,-30.773003 +98956153.214,98.956153,-31.006472,-30.774569 +98956446.182,98.956446,-31.041906,-30.774586 +98956739.151,98.956739,-30.888844,-30.773735 +98957032.119,98.957032,-30.557899,-30.775625 +98957325.088,98.957325,-30.863173,-30.779456 +98957618.056,98.957618,-30.850784,-30.781341 +98957911.025,98.957911,-30.503971,-30.783546 +98958203.993,98.958204,-30.598680,-30.788509 +98958496.962,98.958497,-30.620146,-30.789768 +98958789.930,98.958790,-31.061508,-30.785341 +98959082.899,98.959083,-30.972284,-30.778940 +98959375.867,98.959376,-30.921349,-30.776385 +98959668.836,98.959669,-30.782682,-30.782084 +98959961.804,98.959962,-31.160372,-30.786018 +98960254.773,98.960255,-31.315998,-30.792725 +98960547.741,98.960548,-31.396997,-30.793394 +98960840.710,98.960841,-31.789436,-30.787721 +98961133.678,98.961134,-31.311058,-30.786580 +98961426.647,98.961427,-30.913765,-30.786236 +98961719.615,98.961720,-30.864351,-30.785116 +98962012.584,98.962013,-30.627069,-30.783399 +98962305.552,98.962306,-30.878376,-30.788060 +98962598.521,98.962599,-30.931364,-30.792883 +98962891.489,98.962891,-30.626982,-30.794351 +98963184.458,98.963184,-30.854921,-30.795834 +98963477.426,98.963477,-31.090603,-30.792635 +98963770.395,98.963770,-30.727373,-30.789450 +98964063.363,98.964063,-30.599092,-30.786020 +98964356.332,98.964356,-30.950430,-30.784400 +98964649.300,98.964649,-31.164848,-30.781392 +98964942.269,98.964942,-30.566568,-30.778785 +98965235.237,98.965235,-30.424143,-30.776386 +98965528.206,98.965528,-31.146431,-30.776031 +98965821.174,98.965821,-30.927185,-30.778470 +98966114.143,98.966114,-30.432852,-30.776351 +98966407.111,98.966407,-30.559664,-30.778778 +98966700.080,98.966700,-30.818302,-30.783910 +98966993.048,98.966993,-30.989832,-30.785431 +98967286.017,98.967286,-31.347527,-30.787298 +98967578.985,98.967579,-30.682035,-30.792138 +98967871.954,98.967872,-30.562756,-30.789779 +98968164.922,98.968165,-30.742846,-30.783368 +98968457.891,98.968458,-30.849607,-30.783866 +98968750.859,98.968751,-30.334581,-30.786090 +98969043.828,98.969044,-30.129780,-30.790495 +98969336.796,98.969337,-30.529423,-30.792399 +98969629.765,98.969630,-30.929958,-30.785480 +98969922.733,98.969923,-31.434776,-30.780346 +98970215.702,98.970216,-30.514704,-30.790603 +98970508.670,98.970509,-30.161589,-30.790356 +98970801.639,98.970802,-30.270100,-30.786588 +98971094.607,98.971095,-30.360012,-30.788755 +98971387.576,98.971388,-30.288889,-30.788662 +98971680.544,98.971681,-30.543072,-30.789642 +98971973.513,98.971974,-30.703714,-30.790192 +98972266.481,98.972266,-30.559252,-30.788914 +98972559.450,98.972559,-30.313820,-30.786373 +98972852.418,98.972852,-31.072962,-30.785303 +98973145.387,98.973145,-31.252058,-30.788257 +98973438.355,98.973438,-30.947172,-30.794156 +98973731.324,98.973731,-30.663694,-30.795288 +98974024.292,98.974024,-31.152767,-30.793894 +98974317.261,98.974317,-31.068937,-30.795973 +98974610.229,98.974610,-31.024012,-30.798041 +98974903.198,98.974903,-30.700876,-30.799396 +98975196.167,98.975196,-30.457087,-30.797877 +98975489.135,98.975489,-30.712948,-30.796592 +98975782.104,98.975782,-30.737894,-30.800207 +98976075.072,98.976075,-31.401709,-30.804191 +98976368.041,98.976368,-31.116579,-30.800278 +98976661.009,98.976661,-30.785387,-30.796615 +98976953.978,98.976954,-31.106840,-30.796821 +98977246.946,98.977247,-31.156837,-30.800054 +98977539.915,98.977540,-30.848158,-30.803439 +98977832.883,98.977833,-30.902964,-30.807641 +98978125.852,98.978126,-31.276306,-30.804979 +98978418.820,98.978419,-31.038996,-30.796912 +98978711.789,98.978712,-30.243492,-30.796882 +98979004.757,98.979005,-29.829500,-30.796778 +98979297.726,98.979298,-30.443394,-30.796924 +98979590.694,98.979591,-31.318148,-30.796188 +98979883.663,98.979884,-31.132040,-30.791532 +98980176.631,98.980177,-31.058886,-30.782030 +98980469.600,98.980470,-30.770697,-30.770980 +98980762.568,98.980763,-30.769438,-30.764875 +98981055.537,98.981056,-30.589228,-30.759774 +98981348.505,98.981349,-30.457151,-30.760933 +98981641.474,98.981641,-30.485056,-30.760826 +98981934.442,98.981934,-30.698706,-30.757851 +98982227.411,98.982227,-30.872673,-30.760705 +98982520.379,98.982520,-31.064684,-30.761633 +98982813.348,98.982813,-31.334093,-30.759186 +98983106.316,98.983106,-31.198538,-30.759129 +98983399.285,98.983399,-30.642199,-30.757727 +98983692.253,98.983692,-30.534075,-30.757803 +98983985.222,98.983985,-30.876583,-30.758434 +98984278.190,98.984278,-31.013388,-30.759527 +98984571.159,98.984571,-30.642115,-30.755656 +98984864.127,98.984864,-30.614054,-30.752506 +98985157.096,98.985157,-30.716261,-30.756927 +98985450.064,98.985450,-31.444708,-30.753660 +98985743.033,98.985743,-31.430588,-30.748942 +98986036.001,98.986036,-30.779902,-30.746452 +98986328.970,98.986329,-30.814882,-30.748741 +98986621.938,98.986622,-31.030266,-30.750145 +98986914.907,98.986915,-30.692541,-30.748242 +98987207.875,98.987208,-30.649673,-30.746851 +98987500.844,98.987501,-30.941095,-30.743296 +98987793.812,98.987794,-30.791706,-30.746186 +98988086.781,98.988087,-30.232040,-30.748478 +98988379.749,98.988380,-30.639763,-30.750916 +98988672.718,98.988673,-30.585445,-30.750936 +98988965.686,98.988966,-30.499300,-30.755391 +98989258.655,98.989259,-30.948536,-30.762341 +98989551.623,98.989552,-30.505934,-30.767007 +98989844.592,98.989845,-30.455524,-30.768444 +98990137.560,98.990138,-29.988110,-30.764341 +98990430.529,98.990431,-29.745516,-30.764589 +98990723.497,98.990723,-30.734007,-30.767304 +98991016.466,98.991016,-30.991087,-30.770180 +98991309.434,98.991309,-30.950272,-30.769472 +98991602.403,98.991602,-31.030817,-30.774961 +98991895.371,98.991895,-30.998878,-30.779421 +98992188.340,98.992188,-30.981829,-30.779712 +98992481.308,98.992481,-30.627420,-30.780302 +98992774.277,98.992774,-30.772575,-30.784970 +98993067.245,98.993067,-30.901453,-30.783128 +98993360.214,98.993360,-31.486811,-30.780843 +98993653.182,98.993653,-30.972567,-30.777913 +98993946.151,98.993946,-30.811104,-30.776674 +98994239.119,98.994239,-30.966230,-30.772977 +98994532.088,98.994532,-30.948320,-30.771486 +98994825.056,98.994825,-31.069780,-30.773057 +98995118.025,98.995118,-30.843737,-30.774329 +98995410.993,98.995411,-30.859272,-30.775452 +98995703.962,98.995704,-31.119511,-30.776117 +98995996.930,98.995997,-30.993834,-30.778237 +98996289.899,98.996290,-30.478050,-30.774660 +98996582.867,98.996583,-30.541174,-30.769915 +98996875.836,98.996876,-30.917833,-30.766568 +98997168.804,98.997169,-30.998472,-30.761492 +98997461.773,98.997462,-31.361105,-30.758304 +98997754.741,98.997755,-31.475313,-30.760005 +98998047.710,98.998048,-30.176460,-30.763009 +98998340.678,98.998341,-29.654303,-30.761702 +98998633.647,98.998634,-30.616133,-30.762101 +98998926.615,98.998927,-31.046522,-30.765123 +98999219.584,98.999220,-30.993170,-30.772163 +98999512.552,98.999513,-30.818739,-30.775575 +98999805.521,98.999806,-30.188889,-30.773504 +99000098.490,99.000098,-29.932768,-30.771085 +99000391.458,99.000391,-29.871283,-30.763480 +99000684.427,99.000684,-30.524075,-30.755002 +99000977.395,99.000977,-30.983763,-30.755931 +99001270.364,99.001270,-31.495264,-30.755406 +99001563.332,99.001563,-30.898602,-30.753329 +99001856.301,99.001856,-30.465673,-30.756574 +99002149.269,99.002149,-31.023878,-30.760767 +99002442.238,99.002442,-31.010923,-30.760798 +99002735.206,99.002735,-30.596041,-30.758574 +99003028.175,99.003028,-30.619385,-30.755321 +99003321.143,99.003321,-30.662798,-30.754143 +99003614.112,99.003614,-31.101974,-30.752395 +99003907.080,99.003907,-30.814037,-30.750644 +99004200.049,99.004200,-30.745834,-30.749536 +99004493.017,99.004493,-30.428652,-30.750247 +99004785.986,99.004786,-30.714628,-30.752323 +99005078.954,99.005079,-31.189224,-30.753740 +99005371.923,99.005372,-30.028036,-30.750143 +99005664.891,99.005665,-30.490593,-30.745989 +99005957.860,99.005958,-30.585442,-30.742171 +99006250.828,99.006251,-30.734842,-30.743488 +99006543.797,99.006544,-30.747751,-30.744254 +99006836.765,99.006837,-30.561163,-30.741819 +99007129.734,99.007130,-30.792929,-30.736094 +99007422.702,99.007423,-30.821163,-30.733580 +99007715.671,99.007716,-31.090948,-30.730255 +99008008.639,99.008009,-30.874697,-30.726199 +99008301.608,99.008302,-31.089762,-30.726378 +99008594.576,99.008595,-30.852379,-30.722821 +99008887.545,99.008888,-30.927626,-30.717602 +99009180.513,99.009181,-31.043112,-30.717514 +99009473.482,99.009473,-31.181669,-30.713674 +99009766.450,99.009766,-31.139194,-30.715466 +99010059.419,99.010059,-30.824217,-30.716779 +99010352.387,99.010352,-30.546808,-30.722327 +99010645.356,99.010645,-30.498003,-30.729241 +99010938.324,99.010938,-30.636370,-30.731082 +99011231.293,99.011231,-30.272669,-30.731935 +99011524.261,99.011524,-31.019768,-30.729828 +99011817.230,99.011817,-31.164455,-30.722864 +99012110.198,99.012110,-30.743166,-30.722642 +99012403.167,99.012403,-30.636713,-30.721167 +99012696.135,99.012696,-30.929565,-30.722416 +99012989.104,99.012989,-30.811256,-30.723265 +99013282.072,99.013282,-30.916582,-30.724151 +99013575.041,99.013575,-30.548710,-30.719072 +99013868.009,99.013868,-30.501394,-30.712690 +99014160.978,99.014161,-30.632750,-30.710530 +99014453.946,99.014454,-30.855513,-30.709209 +99014746.915,99.014747,-31.258291,-30.707931 +99015039.883,99.015040,-30.875658,-30.706327 +99015332.852,99.015333,-30.602562,-30.704723 +99015625.820,99.015626,-30.803690,-30.703703 +99015918.789,99.015919,-31.035738,-30.698844 +99016211.757,99.016212,-30.869619,-30.693873 +99016504.726,99.016505,-30.464487,-30.697078 +99016797.694,99.016798,-30.347750,-30.695548 +99017090.663,99.017091,-30.412912,-30.690474 +99017383.631,99.017384,-30.702820,-30.683481 +99017676.600,99.017677,-31.092726,-30.676504 +99017969.568,99.017970,-31.350443,-30.665547 +99018262.537,99.018263,-31.079391,-30.667338 +99018555.505,99.018556,-31.097513,-30.673631 +99018848.474,99.018848,-30.627279,-30.668368 +99019141.442,99.019141,-30.685480,-30.664283 +99019434.411,99.019434,-30.899824,-30.657640 +99019727.379,99.019727,-31.007986,-30.655274 +99020020.348,99.020020,-30.786104,-30.658755 +99020313.316,99.020313,-30.067028,-30.662388 +99020606.285,99.020606,-29.738955,-30.667067 +99020899.253,99.020899,-30.899057,-30.670472 +99021192.222,99.021192,-30.520573,-30.671372 +99021485.190,99.021485,-30.199194,-30.664217 +99021778.159,99.021778,-30.924360,-30.659022 +99022071.127,99.022071,-31.305887,-30.658425 +99022364.096,99.022364,-30.876913,-30.654295 +99022657.064,99.022657,-30.749588,-30.650312 +99022950.033,99.022950,-30.852804,-30.647651 +99023243.001,99.023243,-31.023405,-30.648689 +99023535.970,99.023536,-30.414810,-30.653725 +99023828.938,99.023829,-30.311632,-30.653274 +99024121.907,99.024122,-30.723085,-30.644908 +99024414.875,99.024415,-31.118196,-30.641052 +99024707.844,99.024708,-30.928705,-30.642183 +99025000.812,99.025001,-30.806219,-30.638685 +99025293.781,99.025294,-30.253166,-30.631562 +99025586.750,99.025587,-30.823179,-30.633979 +99025879.718,99.025880,-30.857458,-30.631604 +99026172.687,99.026173,-30.965826,-30.626482 +99026465.655,99.026466,-30.922911,-30.620568 +99026758.624,99.026759,-30.687769,-30.617799 +99027051.592,99.027052,-29.978603,-30.613953 +99027344.561,99.027345,-30.324326,-30.607031 +99027637.529,99.027638,-30.487707,-30.602110 +99027930.498,99.027930,-30.261559,-30.596388 +99028223.466,99.028223,-30.253984,-30.595290 +99028516.435,99.028516,-30.186398,-30.593499 +99028809.403,99.028809,-29.942358,-30.591170 +99029102.372,99.029102,-30.487747,-30.586803 +99029395.340,99.029395,-30.426264,-30.583494 +99029688.309,99.029688,-30.746462,-30.578717 +99029981.277,99.029981,-30.628300,-30.571683 +99030274.246,99.030274,-30.683123,-30.568674 +99030567.214,99.030567,-30.575962,-30.565815 +99030860.183,99.030860,-30.994083,-30.563789 +99031153.151,99.031153,-31.116858,-30.563777 +99031446.120,99.031446,-30.656813,-30.567472 +99031739.088,99.031739,-30.111971,-30.565457 +99032032.057,99.032032,-30.966570,-30.562128 +99032325.025,99.032325,-30.772333,-30.559604 +99032617.994,99.032618,-30.798243,-30.556849 +99032910.962,99.032911,-30.891762,-30.557064 +99033203.931,99.033204,-31.029791,-30.557894 +99033496.899,99.033497,-30.727362,-30.555151 +99033789.868,99.033790,-30.131905,-30.555175 +99034082.836,99.034083,-30.518473,-30.553714 +99034375.805,99.034376,-30.778299,-30.547263 +99034668.773,99.034669,-30.767117,-30.541509 +99034961.742,99.034962,-30.837229,-30.538239 +99035254.710,99.035255,-30.622673,-30.538407 +99035547.679,99.035548,-30.716755,-30.536751 +99035840.647,99.035841,-30.442297,-30.533227 +99036133.616,99.036134,-30.319586,-30.529695 +99036426.584,99.036427,-30.917192,-30.525962 +99036719.553,99.036720,-30.343538,-30.522398 +99037012.521,99.037013,-30.241312,-30.522934 +99037305.490,99.037305,-30.074759,-30.523891 +99037598.458,99.037598,-30.366135,-30.523310 +99037891.427,99.037891,-29.964773,-30.524930 +99038184.395,99.038184,-30.401327,-30.517083 +99038477.364,99.038477,-30.396589,-30.512482 +99038770.332,99.038770,-29.956858,-30.506292 +99039063.301,99.039063,-30.474894,-30.505167 +99039356.269,99.039356,-30.107502,-30.502255 +99039649.238,99.039649,-30.494661,-30.497741 +99039942.206,99.039942,-30.639643,-30.491799 +99040235.175,99.040235,-30.375607,-30.487834 +99040528.143,99.040528,-30.440992,-30.488004 +99040821.112,99.040821,-31.000578,-30.492678 +99041114.080,99.041114,-31.118252,-30.485582 +99041407.049,99.041407,-30.446663,-30.482366 +99041700.017,99.041700,-30.205370,-30.480643 +99041992.986,99.041993,-30.388132,-30.470661 +99042285.954,99.042286,-30.448107,-30.460292 +99042578.923,99.042579,-30.455282,-30.455947 +99042871.891,99.042872,-30.250065,-30.453586 +99043164.860,99.043165,-30.763121,-30.446053 +99043457.828,99.043458,-31.416283,-30.440365 +99043750.797,99.043751,-31.034025,-30.437854 +99044043.765,99.044044,-29.761890,-30.433811 +99044336.734,99.044337,-30.235975,-30.427988 +99044629.702,99.044630,-30.578753,-30.424292 +99044922.671,99.044923,-30.252592,-30.420977 +99045215.639,99.045216,-30.201916,-30.415707 +99045508.608,99.045509,-30.326113,-30.413642 +99045801.576,99.045802,-30.186571,-30.406924 +99046094.545,99.046095,-29.941042,-30.400121 +99046387.513,99.046388,-29.974268,-30.396625 +99046680.482,99.046680,-30.373516,-30.397350 +99046973.450,99.046973,-30.069712,-30.397488 +99047266.419,99.047266,-29.901981,-30.400951 +99047559.387,99.047559,-30.164692,-30.402196 +99047852.356,99.047852,-30.291410,-30.404942 +99048145.324,99.048145,-30.717232,-30.405400 +99048438.293,99.048438,-30.823099,-30.405211 +99048731.261,99.048731,-30.526123,-30.406776 +99049024.230,99.049024,-30.324699,-30.407134 +99049317.198,99.049317,-30.566563,-30.406607 +99049610.167,99.049610,-30.488464,-30.406074 +99049903.135,99.049903,-30.160925,-30.401701 +99050196.104,99.050196,-30.407663,-30.398642 +99050489.073,99.050489,-30.173592,-30.397781 +99050782.041,99.050782,-30.232937,-30.397653 +99051075.010,99.051075,-30.634626,-30.396226 +99051367.978,99.051368,-30.773703,-30.389656 +99051660.947,99.051661,-30.723686,-30.388298 +99051953.915,99.051954,-30.669996,-30.391333 +99052246.884,99.052247,-30.396742,-30.385503 +99052539.852,99.052540,-30.268446,-30.381016 +99052832.821,99.052833,-30.961773,-30.374112 +99053125.789,99.053126,-30.933495,-30.367671 +99053418.758,99.053419,-30.526365,-30.361299 +99053711.726,99.053712,-30.551943,-30.358332 +99054004.695,99.054005,-30.308079,-30.359914 +99054297.663,99.054298,-29.814310,-30.358293 +99054590.632,99.054591,-30.081831,-30.355640 +99054883.600,99.054884,-30.759510,-30.352101 +99055176.569,99.055177,-30.900616,-30.345007 +99055469.537,99.055470,-30.378073,-30.340427 +99055762.506,99.055763,-30.318283,-30.334108 +99056055.474,99.056055,-30.523739,-30.330638 +99056348.443,99.056348,-30.348864,-30.332090 +99056641.411,99.056641,-30.007681,-30.327356 +99056934.380,99.056934,-30.418844,-30.326528 +99057227.348,99.057227,-30.542740,-30.325636 +99057520.317,99.057520,-30.620560,-30.318254 +99057813.285,99.057813,-31.353523,-30.311558 +99058106.254,99.058106,-30.212139,-30.312519 +99058399.222,99.058399,-30.415138,-30.313134 +99058692.191,99.058692,-30.221151,-30.310038 +99058985.159,99.058985,-30.471647,-30.312262 +99059278.128,99.059278,-30.288124,-30.310270 +99059571.096,99.059571,-30.269815,-30.311009 +99059864.065,99.059864,-30.176664,-30.305964 +99060157.033,99.060157,-30.240349,-30.300402 +99060450.002,99.060450,-30.088190,-30.298169 +99060742.970,99.060743,-30.314085,-30.298248 +99061035.939,99.061036,-29.943871,-30.293228 +99061328.907,99.061329,-30.097490,-30.284734 +99061621.876,99.061622,-29.983463,-30.280704 +99061914.844,99.061915,-29.623730,-30.280107 +99062207.813,99.062208,-29.854145,-30.276428 +99062500.781,99.062501,-30.266413,-30.269383 +99062793.750,99.062794,-30.416462,-30.261981 +99063086.718,99.063087,-29.846554,-30.255784 +99063379.687,99.063380,-30.213303,-30.251491 +99063672.655,99.063673,-30.085449,-30.242501 +99063965.624,99.063966,-29.804193,-30.230148 +99064258.592,99.064259,-29.944057,-30.229534 +99064551.561,99.064552,-30.561602,-30.228092 +99064844.529,99.064845,-30.446321,-30.221360 +99065137.498,99.065137,-30.080832,-30.218351 +99065430.466,99.065430,-29.988836,-30.215857 +99065723.435,99.065723,-29.913313,-30.210624 +99066016.403,99.066016,-29.929445,-30.202313 +99066309.372,99.066309,-30.451670,-30.200182 +99066602.340,99.066602,-31.036474,-30.196900 +99066895.309,99.066895,-30.707949,-30.190095 +99067188.277,99.067188,-30.432131,-30.185730 +99067481.246,99.067481,-30.495266,-30.181460 +99067774.214,99.067774,-30.889238,-30.178184 +99068067.183,99.068067,-30.322702,-30.175897 +99068360.151,99.068360,-30.229013,-30.169253 +99068653.120,99.068653,-30.395239,-30.159446 +99068946.088,99.068946,-29.986597,-30.149722 +99069239.057,99.069239,-30.414892,-30.146072 +99069532.025,99.069532,-30.353456,-30.143072 +99069824.994,99.069825,-30.142248,-30.138594 +99070117.962,99.070118,-30.207640,-30.133926 +99070410.931,99.070411,-30.559042,-30.127802 +99070703.899,99.070704,-30.557707,-30.122458 +99070996.868,99.070997,-30.775330,-30.116535 +99071289.836,99.071290,-30.165693,-30.105479 +99071582.805,99.071583,-30.463263,-30.094791 +99071875.773,99.071876,-30.520708,-30.081846 +99072168.742,99.072169,-30.138363,-30.070730 +99072461.710,99.072462,-30.147211,-30.060754 +99072754.679,99.072755,-29.864218,-30.054493 +99073047.647,99.073048,-29.995939,-30.045570 +99073340.616,99.073341,-30.115673,-30.033820 +99073633.584,99.073634,-30.306135,-30.026305 +99073926.553,99.073927,-30.342604,-30.014773 +99074219.521,99.074220,-30.293962,-30.003821 +99074512.490,99.074512,-30.395298,-29.999809 +99074805.458,99.074805,-30.264027,-29.995328 +99075098.427,99.075098,-29.866720,-29.986449 +99075391.396,99.075391,-30.000204,-29.974638 +99075684.364,99.075684,-29.861977,-29.965516 +99075977.333,99.075977,-29.980206,-29.956447 +99076270.301,99.076270,-30.522558,-29.945593 +99076563.270,99.076563,-30.231794,-29.937422 +99076856.238,99.076856,-30.231094,-29.931583 +99077149.207,99.077149,-30.123117,-29.923656 +99077442.175,99.077442,-29.211237,-29.911921 +99077735.144,99.077735,-29.524841,-29.895464 +99078028.112,99.078028,-30.088106,-29.877667 +99078321.081,99.078321,-30.488089,-29.871454 +99078614.049,99.078614,-29.984381,-29.858903 +99078907.018,99.078907,-30.247057,-29.847115 +99079199.986,99.079200,-30.200397,-29.834339 +99079492.955,99.079493,-30.205172,-29.822976 +99079785.923,99.079786,-29.827299,-29.807463 +99080078.892,99.080079,-29.885618,-29.793193 +99080371.860,99.080372,-30.075022,-29.780216 +99080664.829,99.080665,-30.452074,-29.769022 +99080957.797,99.080958,-30.258867,-29.760010 +99081250.766,99.081251,-29.898314,-29.749837 +99081543.734,99.081544,-29.908058,-29.737427 +99081836.703,99.081837,-30.125757,-29.724234 +99082129.671,99.082130,-29.899723,-29.710200 +99082422.640,99.082423,-29.544157,-29.695370 +99082715.608,99.082716,-29.507307,-29.675193 +99083008.577,99.083009,-29.476091,-29.649420 +99083301.545,99.083302,-30.147543,-29.627671 +99083594.514,99.083595,-30.045280,-29.603638 +99083887.482,99.083887,-29.365984,-29.582328 +99084180.451,99.084180,-29.687002,-29.560280 +99084473.419,99.084473,-30.042490,-29.540544 +99084766.388,99.084766,-29.678345,-29.519424 +99085059.356,99.085059,-29.855911,-29.500801 +99085352.325,99.085352,-29.874058,-29.480555 +99085645.293,99.085645,-29.646402,-29.453998 +99085938.262,99.085938,-29.179464,-29.426468 +99086231.230,99.086231,-29.674496,-29.403869 +99086524.199,99.086524,-29.566929,-29.379610 +99086817.167,99.086817,-29.496595,-29.344825 +99087110.136,99.087110,-29.523621,-29.305646 +99087403.104,99.087403,-29.385691,-29.269751 +99087696.073,99.087696,-29.738932,-29.232418 +99087989.041,99.087989,-29.981123,-29.198745 +99088282.010,99.088282,-29.791489,-29.169504 +99088574.978,99.088575,-29.487564,-29.142488 +99088867.947,99.088868,-29.274065,-29.116214 +99089160.915,99.089161,-29.832802,-29.092198 +99089453.884,99.089454,-30.135757,-29.063451 +99089746.852,99.089747,-29.870001,-29.033637 +99090039.821,99.090040,-29.561144,-28.996770 +99090332.789,99.090333,-29.594641,-28.961071 +99090625.758,99.090626,-29.489897,-28.925664 +99090918.726,99.090919,-29.470982,-28.888517 +99091211.695,99.091212,-29.193687,-28.848492 +99091504.663,99.091505,-29.331919,-28.816030 +99091797.632,99.091798,-29.042610,-28.779671 +99092090.600,99.092091,-29.202406,-28.740182 +99092383.569,99.092384,-29.126968,-28.703357 +99092676.537,99.092677,-29.450163,-28.674657 +99092969.506,99.092970,-29.667301,-28.643337 +99093262.474,99.093262,-29.303898,-28.606459 +99093555.443,99.093555,-29.502815,-28.567127 +99093848.411,99.093848,-29.056267,-28.519016 +99094141.380,99.094141,-28.940538,-28.471633 +99094434.348,99.094434,-29.317495,-28.423362 +99094727.317,99.094727,-29.497072,-28.381138 +99095020.285,99.095020,-29.506916,-28.341980 +99095313.254,99.095313,-29.255526,-28.309117 +99095606.222,99.095606,-29.185263,-28.272475 +99095899.191,99.095899,-29.143461,-28.230475 +99096192.159,99.096192,-29.091600,-28.182674 +99096485.128,99.096485,-29.266157,-28.136584 +99096778.096,99.096778,-29.262026,-28.087055 +99097071.065,99.097071,-29.346754,-28.030445 +99097364.033,99.097364,-28.997044,-27.967900 +99097657.002,99.097657,-28.546059,-27.908280 +99097949.970,99.097950,-28.832245,-27.847588 +99098242.939,99.098243,-29.377718,-27.784341 +99098535.907,99.098536,-28.803272,-27.719929 +99098828.876,99.098829,-28.745207,-27.662719 +99099121.844,99.099122,-28.810198,-27.603617 +99099414.813,99.099415,-28.831581,-27.548834 +99099707.781,99.099708,-28.385180,-27.498343 +99100000.750,99.100001,-28.442146,-27.439643 +99100293.719,99.100294,-28.613398,-27.380754 +99100586.687,99.100587,-28.690836,-27.319175 +99100879.656,99.100880,-29.099592,-27.253861 +99101172.624,99.101173,-28.688543,-27.190732 +99101465.593,99.101466,-28.560141,-27.132136 +99101758.561,99.101759,-28.396532,-27.072553 +99102051.530,99.102052,-28.060480,-27.004433 +99102344.498,99.102344,-28.138523,-26.941730 +99102637.467,99.102637,-27.891985,-26.884594 +99102930.435,99.102930,-27.477917,-26.824110 +99103223.404,99.103223,-27.491905,-26.759115 +99103516.372,99.103516,-27.494947,-26.696983 +99103809.341,99.103809,-27.647772,-26.633256 +99104102.309,99.104102,-27.414173,-26.574220 +99104395.278,99.104395,-27.686653,-26.518466 +99104688.246,99.104688,-27.901934,-26.462491 +99104981.215,99.104981,-28.068916,-26.408253 +99105274.183,99.105274,-27.696226,-26.348496 +99105567.152,99.105567,-27.100422,-26.281233 +99105860.120,99.105860,-26.972198,-26.215804 +99106153.089,99.106153,-27.370705,-26.153962 +99106446.057,99.106446,-27.492008,-26.094460 +99106739.026,99.106739,-26.864359,-26.032773 +99107031.994,99.107032,-26.395464,-25.960388 +99107324.963,99.107325,-26.498608,-25.885862 +99107617.931,99.107618,-26.398735,-25.810864 +99107910.900,99.107911,-26.803988,-25.737762 +99108203.868,99.108204,-26.900673,-25.668487 +99108496.837,99.108497,-27.025278,-25.591285 +99108789.805,99.108790,-27.153847,-25.514292 +99109082.774,99.109083,-27.134859,-25.436391 +99109375.742,99.109376,-26.924519,-25.355478 +99109668.711,99.109669,-26.792931,-25.262274 +99109961.679,99.109962,-26.149714,-25.172627 +99110254.648,99.110255,-26.237976,-25.090942 +99110547.616,99.110548,-26.375544,-25.010597 +99110840.585,99.110841,-26.222174,-24.936067 +99111133.553,99.111134,-26.064417,-24.853861 +99111426.522,99.111427,-26.378016,-24.763838 +99111719.490,99.111719,-26.176971,-24.665406 +99112012.459,99.112012,-25.944195,-24.568900 +99112305.427,99.112305,-25.972509,-24.477044 +99112598.396,99.112598,-26.593456,-24.384312 +99112891.364,99.112891,-26.239367,-24.290940 +99113184.333,99.113184,-25.854273,-24.194632 +99113477.301,99.113477,-25.699848,-24.095618 +99113770.270,99.113770,-25.150473,-23.994905 +99114063.238,99.114063,-25.174459,-23.891078 +99114356.207,99.114356,-25.070116,-23.776334 +99114649.175,99.114649,-25.470964,-23.649794 +99114942.144,99.114942,-25.618696,-23.515542 +99115235.112,99.115235,-25.925196,-23.382911 +99115528.081,99.115528,-25.665071,-23.254895 +99115821.049,99.115821,-25.208530,-23.131429 +99116114.018,99.116114,-24.836721,-23.010026 +99116406.986,99.116407,-25.075514,-22.870920 +99116699.955,99.116700,-24.734814,-22.705735 +99116992.923,99.116993,-24.270756,-22.528973 +99117285.892,99.117286,-23.872507,-22.353126 +99117578.860,99.117579,-23.744070,-22.172888 +99117871.829,99.117872,-23.729921,-21.997962 +99118164.797,99.118165,-23.686659,-21.844069 +99118457.766,99.118458,-23.662056,-21.719120 +99118750.734,99.118751,-23.899136,-21.614949 +99119043.703,99.119044,-23.809031,-21.513478 +99119336.671,99.119337,-24.007114,-21.415311 +99119629.640,99.119630,-24.235527,-21.312676 +99119922.608,99.119923,-23.608057,-21.209962 +99120215.577,99.120216,-23.566246,-21.109511 +99120508.545,99.120509,-23.411600,-21.015487 +99120801.514,99.120802,-23.233387,-20.927320 +99121094.482,99.121094,-23.263020,-20.838756 +99121387.451,99.121387,-23.393980,-20.744482 +99121680.419,99.121680,-23.293547,-20.641895 +99121973.388,99.121973,-22.822245,-20.540359 +99122266.356,99.122266,-23.015059,-20.438532 +99122559.325,99.122559,-23.206120,-20.334688 +99122852.293,99.122852,-22.961121,-20.231426 +99123145.262,99.123145,-22.659859,-20.133421 +99123438.230,99.123438,-22.898773,-20.040590 +99123731.199,99.123731,-22.739040,-19.952672 +99124024.167,99.124024,-22.816660,-19.863265 +99124317.136,99.124317,-23.036097,-19.769498 +99124610.104,99.124610,-23.052202,-19.672440 +99124903.073,99.124903,-23.041069,-19.572151 +99125196.042,99.125196,-22.697222,-19.472101 +99125489.010,99.125489,-22.228222,-19.371800 +99125781.979,99.125782,-22.232264,-19.280572 +99126074.947,99.126075,-22.288511,-19.187182 +99126367.916,99.126368,-22.472002,-19.088893 +99126660.884,99.126661,-22.275431,-18.986815 +99126953.853,99.126954,-21.634752,-18.887481 +99127246.821,99.127247,-21.471090,-18.785820 +99127539.790,99.127540,-21.363241,-18.685076 +99127832.758,99.127833,-21.448751,-18.581729 +99128125.727,99.128126,-21.622828,-18.474768 +99128418.695,99.128419,-21.128969,-18.367583 +99128711.664,99.128712,-21.030418,-18.268489 +99129004.632,99.129005,-20.889826,-18.179586 +99129297.601,99.129298,-20.753292,-18.092783 +99129590.569,99.129591,-20.164352,-18.010203 +99129883.538,99.129884,-20.206051,-17.930064 +99130176.506,99.130177,-20.452078,-17.855291 +99130469.475,99.130469,-20.448111,-17.786277 +99130762.443,99.130762,-20.652327,-17.712077 +99131055.412,99.131055,-20.208662,-17.637770 +99131348.380,99.131348,-19.751640,-17.566017 +99131641.349,99.131641,-19.335823,-17.497494 +99131934.317,99.131934,-19.295519,-17.427387 +99132227.286,99.132227,-19.420025,-17.363870 +99132520.254,99.132520,-19.292553,-17.298231 +99132813.223,99.132813,-19.213909,-17.231384 +99133106.191,99.133106,-19.024977,-17.167281 +99133399.160,99.133399,-18.794231,-17.101258 +99133692.128,99.133692,-18.652367,-17.031053 +99133985.097,99.133985,-18.400009,-16.957994 +99134278.065,99.134278,-17.891644,-16.888063 +99134571.034,99.134571,-17.411808,-16.819718 +99134864.002,99.134864,-17.058855,-16.750667 +99135156.971,99.135157,-16.983290,-16.681019 +99135449.939,99.135450,-16.990948,-16.616083 +99135742.908,99.135743,-17.016272,-16.544487 +99136035.876,99.136036,-16.967474,-16.472179 +99136328.845,99.136329,-16.283781,-16.413359 +99136621.813,99.136622,-15.435401,-16.351105 +99136914.782,99.136915,-14.988358,-16.286874 +99137207.750,99.137208,-14.843287,-16.221448 +99137500.719,99.137501,-14.555801,-16.155340 +99137793.687,99.137794,-14.492971,-16.085862 +99138086.656,99.138087,-14.880788,-16.016515 +99138379.624,99.138380,-15.637209,-15.944510 +99138672.593,99.138673,-16.251795,-15.869916 +99138965.561,99.138966,-16.259134,-15.798236 +99139258.530,99.139259,-16.304272,-15.726347 +99139551.498,99.139551,-16.027691,-15.656977 +99139844.467,99.139844,-15.903133,-15.586935 +99140137.435,99.140137,-15.901753,-15.516228 +99140430.404,99.140430,-16.090656,-15.447863 +99140723.372,99.140723,-16.272648,-15.383061 +99141016.341,99.141016,-16.192757,-15.321035 +99141309.309,99.141309,-15.827494,-15.257171 +99141602.278,99.141602,-15.378858,-15.189784 +99141895.246,99.141895,-15.316327,-15.124360 +99142188.215,99.142188,-15.191071,-15.062171 +99142481.183,99.142481,-15.016353,-14.999198 +99142774.152,99.142774,-14.928936,-14.941985 +99143067.120,99.143067,-15.026238,-14.881510 +99143360.089,99.143360,-15.159107,-14.819463 +99143653.057,99.143653,-15.296376,-14.760730 +99143946.026,99.143946,-15.153164,-14.702896 +99144238.994,99.144239,-14.858370,-14.642778 +99144531.963,99.144532,-14.640320,-14.579989 +99144824.931,99.144825,-14.420961,-14.523319 +99145117.900,99.145118,-14.342366,-14.462353 +99145410.868,99.145411,-14.219860,-14.399261 +99145703.837,99.145704,-14.490560,-14.340228 +99145996.805,99.145997,-14.300098,-14.279323 +99146289.774,99.146290,-14.018070,-14.220373 +99146582.742,99.146583,-13.770697,-14.164235 +99146875.711,99.146876,-13.758889,-14.107459 +99147168.679,99.147169,-13.544328,-14.047721 +99147461.648,99.147462,-13.490913,-13.983965 +99147754.616,99.147755,-13.283655,-13.918677 +99148047.585,99.148048,-13.059155,-13.857075 +99148340.553,99.148341,-12.951786,-13.802328 +99148633.522,99.148634,-13.184955,-13.751718 +99148926.490,99.148926,-13.551702,-13.698418 +99149219.459,99.149219,-13.566128,-13.638046 +99149512.427,99.149512,-13.682414,-13.580572 +99149805.396,99.149805,-13.722631,-13.530886 +99150098.365,99.150098,-13.899151,-13.481123 +99150391.333,99.150391,-14.163868,-13.428538 +99150684.302,99.150684,-13.808333,-13.373880 +99150977.270,99.150977,-13.723764,-13.319030 +99151270.239,99.151270,-13.789001,-13.268475 +99151563.207,99.151563,-13.929529,-13.205864 +99151856.176,99.151856,-13.759120,-13.135822 +99152149.144,99.152149,-14.084622,-13.077519 +99152442.113,99.152442,-13.892309,-13.030704 +99152735.081,99.152735,-13.790651,-12.975990 +99153028.050,99.153028,-13.881209,-12.917730 +99153321.018,99.153321,-13.676556,-12.862003 +99153613.987,99.153614,-13.352454,-12.811977 +99153906.955,99.153907,-13.089399,-12.763973 +99154199.924,99.154200,-13.201998,-12.715590 +99154492.892,99.154493,-13.223803,-12.665871 +99154785.861,99.154786,-13.142319,-12.617290 +99155078.829,99.155079,-13.050072,-12.568056 +99155371.798,99.155372,-13.290812,-12.519100 +99155664.766,99.155665,-12.810854,-12.466163 +99155957.735,99.155958,-12.677511,-12.418808 +99156250.703,99.156251,-13.431900,-12.368363 +99156543.672,99.156544,-13.162189,-12.317818 +99156836.640,99.156837,-12.953980,-12.270525 +99157129.609,99.157130,-12.787107,-12.227068 +99157422.577,99.157423,-12.655262,-12.188050 +99157715.546,99.157716,-12.383064,-12.144694 +99158008.514,99.158009,-12.325258,-12.102823 +99158301.483,99.158301,-12.104790,-12.061562 +99158594.451,99.158594,-11.891794,-12.016651 +99158887.420,99.158887,-12.000679,-11.969541 +99159180.388,99.159180,-11.915899,-11.922853 +99159473.357,99.159473,-12.007497,-11.880520 +99159766.325,99.159766,-11.916546,-11.842518 +99160059.294,99.160059,-11.773179,-11.798084 +99160352.262,99.160352,-11.841996,-11.753909 +99160645.231,99.160645,-11.985620,-11.711010 +99160938.199,99.160938,-12.089655,-11.672289 +99161231.168,99.161231,-11.916336,-11.632587 +99161524.136,99.161524,-11.647673,-11.589106 +99161817.105,99.161817,-11.698543,-11.548700 +99162110.073,99.162110,-11.808493,-11.511223 +99162403.042,99.162403,-11.715178,-11.471124 +99162696.010,99.162696,-12.059253,-11.435793 +99162988.979,99.162989,-11.763761,-11.400571 +99163281.947,99.163282,-11.581193,-11.360605 +99163574.916,99.163575,-11.763882,-11.320876 +99163867.884,99.163868,-11.759384,-11.282832 +99164160.853,99.164161,-11.555738,-11.243826 +99164453.821,99.164454,-11.338907,-11.210015 +99164746.790,99.164747,-11.698490,-11.174196 +99165039.758,99.165040,-11.348083,-11.134439 +99165332.727,99.165333,-11.127964,-11.096021 +99165625.695,99.165626,-11.303435,-11.057728 +99165918.664,99.165919,-11.123659,-11.020782 +99166211.632,99.166212,-11.203420,-10.988217 +99166504.601,99.166505,-11.360390,-10.955834 +99166797.569,99.166798,-11.247746,-10.922056 +99167090.538,99.167091,-10.938980,-10.892457 +99167383.506,99.167384,-10.609618,-10.864764 +99167676.475,99.167676,-10.446831,-10.838065 +99167969.443,99.167969,-10.627317,-10.809889 +99168262.412,99.168262,-11.057609,-10.777107 +99168555.380,99.168555,-11.271591,-10.745498 +99168848.349,99.168848,-11.012794,-10.712070 +99169141.317,99.169141,-10.458320,-10.673197 +99169434.286,99.169434,-10.585827,-10.637943 +99169727.254,99.169727,-11.037575,-10.603557 +99170020.223,99.170020,-10.993076,-10.570808 +99170313.191,99.170313,-10.763217,-10.537697 +99170606.160,99.170606,-10.564569,-10.502876 +99170899.128,99.170899,-10.521995,-10.467352 +99171192.097,99.171192,-10.745198,-10.428908 +99171485.065,99.171485,-9.816438,-10.393728 +99171778.034,99.171778,-9.276794,-10.362135 +99172071.002,99.172071,-9.926615,-10.329943 +99172363.971,99.172364,-10.730032,-10.298466 +99172656.939,99.172657,-10.078613,-10.256090 +99172949.908,99.172950,-9.778633,-10.217736 +99173242.876,99.173243,-9.876446,-10.186798 +99173535.845,99.173536,-10.208275,-10.157744 +99173828.813,99.173829,-10.299745,-10.128079 +99174121.782,99.174122,-10.192087,-10.094115 +99174414.750,99.174415,-9.971565,-10.064444 +99174707.719,99.174708,-9.932616,-10.033072 +99175000.688,99.175001,-9.780711,-10.001739 +99175293.656,99.175294,-9.746675,-9.969263 +99175586.625,99.175587,-9.428633,-9.936034 +99175879.593,99.175880,-9.788019,-9.908269 +99176172.562,99.176173,-9.515184,-9.878928 +99176465.530,99.176466,-9.337008,-9.846534 +99176758.499,99.176758,-9.334076,-9.814327 +99177051.467,99.177051,-9.454289,-9.783798 +99177344.436,99.177344,-9.713453,-9.754593 +99177637.404,99.177637,-9.278241,-9.723151 +99177930.373,99.177930,-9.335314,-9.691997 +99178223.341,99.178223,-9.465797,-9.664181 +99178516.310,99.178516,-9.359304,-9.641469 +99178809.278,99.178809,-9.295172,-9.615851 +99179102.247,99.179102,-9.290448,-9.590659 +99179395.215,99.179395,-9.601597,-9.571007 +99179688.184,99.179688,-9.869588,-9.547148 +99179981.152,99.179981,-9.286237,-9.523435 +99180274.121,99.180274,-9.271085,-9.502622 +99180567.089,99.180567,-9.377518,-9.478551 +99180860.058,99.180860,-9.733456,-9.451776 +99181153.026,99.181153,-9.600465,-9.423574 +99181445.995,99.181446,-9.179548,-9.395957 +99181738.963,99.181739,-9.288674,-9.370817 +99182031.932,99.182032,-9.487602,-9.349122 +99182324.900,99.182325,-9.208423,-9.325325 +99182617.869,99.182618,-9.536671,-9.306490 +99182910.837,99.182911,-9.496250,-9.287070 +99183203.806,99.183204,-9.094770,-9.265632 +99183496.774,99.183497,-9.117805,-9.242241 +99183789.743,99.183790,-9.264093,-9.218470 +99184082.711,99.184083,-9.118068,-9.197505 +99184375.680,99.184376,-9.466125,-9.175020 +99184668.648,99.184669,-9.200173,-9.150523 +99184961.617,99.184962,-8.785179,-9.128123 +99185254.585,99.185255,-8.842570,-9.109103 +99185547.554,99.185548,-8.790519,-9.089979 +99185840.522,99.185841,-8.951586,-9.071535 +99186133.491,99.186133,-9.256700,-9.052152 +99186426.459,99.186426,-9.160944,-9.028571 +99186719.428,99.186719,-8.934458,-9.004370 +99187012.396,99.187012,-9.286572,-8.983225 +99187305.365,99.187305,-9.371503,-8.964765 +99187598.333,99.187598,-9.431227,-8.951981 +99187891.302,99.187891,-9.191182,-8.936323 +99188184.270,99.188184,-8.677878,-8.917383 +99188477.239,99.188477,-8.717394,-8.900290 +99188770.207,99.188770,-8.626822,-8.885129 +99189063.176,99.189063,-8.277193,-8.867861 +99189356.144,99.189356,-8.550483,-8.855413 +99189649.113,99.189649,-8.637298,-8.843167 +99189942.081,99.189942,-8.771925,-8.828580 +99190235.050,99.190235,-8.771926,-8.814950 +99190528.018,99.190528,-8.671690,-8.799970 +99190820.987,99.190821,-8.482418,-8.784867 +99191113.955,99.191114,-8.187002,-8.772050 +99191406.924,99.191407,-8.454574,-8.760390 +99191699.892,99.191700,-8.800183,-8.748471 +99191992.861,99.191993,-8.669266,-8.740021 +99192285.829,99.192286,-8.813786,-8.729551 +99192578.798,99.192579,-7.793200,-8.715034 +99192871.766,99.192872,-8.059232,-8.702593 +99193164.735,99.193165,-8.726683,-8.697054 +99193457.703,99.193458,-8.828017,-8.691100 +99193750.672,99.193751,-8.640368,-8.683026 +99194043.640,99.194044,-8.138038,-8.675430 +99194336.609,99.194337,-8.546581,-8.669367 +99194629.577,99.194630,-8.372459,-8.664594 +99194922.546,99.194923,-8.328363,-8.655024 +99195215.514,99.195216,-8.172001,-8.646744 +99195508.483,99.195508,-8.157592,-8.638746 +99195801.451,99.195801,-8.491769,-8.633591 +99196094.420,99.196094,-8.272985,-8.627530 +99196387.388,99.196387,-8.214652,-8.625179 +99196680.357,99.196680,-8.126938,-8.624616 +99196973.325,99.196973,-8.195307,-8.623692 +99197266.294,99.197266,-8.245097,-8.618771 +99197559.262,99.197559,-7.969873,-8.616139 +99197852.231,99.197852,-7.881037,-8.611908 +99198145.199,99.198145,-8.151909,-8.610334 +99198438.168,99.198438,-8.563632,-8.608231 +99198731.136,99.198731,-8.161211,-8.603634 +99199024.105,99.199024,-8.234549,-8.600710 +99199317.073,99.199317,-8.774420,-8.596092 +99199610.042,99.199610,-8.347793,-8.589880 +99199903.010,99.199903,-8.309922,-8.585906 +99200195.979,99.200196,-8.541102,-8.586903 +99200488.948,99.200489,-8.215461,-8.585960 +99200781.916,99.200782,-7.990477,-8.583045 +99201074.885,99.201075,-7.876237,-8.579685 +99201367.853,99.201368,-7.848331,-8.578410 +99201660.822,99.201661,-7.963327,-8.579504 +99201953.790,99.201954,-8.328770,-8.582787 +99202246.759,99.202247,-8.142090,-8.585360 +99202539.727,99.202540,-8.635201,-8.588060 +99202832.696,99.202833,-8.720191,-8.586948 +99203125.664,99.203126,-8.341310,-8.588181 +99203418.633,99.203419,-8.038178,-8.592834 +99203711.601,99.203712,-8.064733,-8.596031 +99204004.570,99.204005,-8.354841,-8.599771 +99204297.538,99.204298,-8.084406,-8.604548 +99204590.507,99.204591,-7.768235,-8.605855 +99204883.475,99.204883,-8.130949,-8.609220 +99205176.444,99.205176,-8.332263,-8.617229 +99205469.412,99.205469,-8.200107,-8.624370 +99205762.381,99.205762,-8.357293,-8.632696 +99206055.349,99.206055,-8.150582,-8.638996 +99206348.318,99.206348,-7.731960,-8.643457 +99206641.286,99.206641,-7.724874,-8.649729 +99206934.255,99.206934,-7.982633,-8.657162 +99207227.223,99.207227,-8.116139,-8.659120 +99207520.192,99.207520,-8.596125,-8.660456 +99207813.160,99.207813,-8.147100,-8.660831 +99208106.129,99.208106,-7.876965,-8.664132 +99208399.097,99.208399,-8.305919,-8.670793 +99208692.066,99.208692,-8.653954,-8.677748 +99208985.034,99.208985,-8.246224,-8.686514 +99209278.003,99.209278,-8.507242,-8.700867 +99209570.971,99.209571,-8.609581,-8.711700 +99209863.940,99.209864,-8.572669,-8.722501 +99210156.908,99.210157,-8.666109,-8.733700 +99210449.877,99.210450,-8.354271,-8.742879 +99210742.845,99.210743,-8.218143,-8.752743 +99211035.814,99.211036,-8.469860,-8.765077 +99211328.782,99.211329,-8.758149,-8.777918 +99211621.751,99.211622,-8.113400,-8.787518 +99211914.719,99.211915,-8.132979,-8.796699 +99212207.688,99.212208,-8.359377,-8.809229 +99212500.656,99.212501,-8.356817,-8.820764 +99212793.625,99.212794,-8.209103,-8.840520 +99213086.593,99.213087,-8.900852,-8.857059 +99213379.562,99.213380,-8.919189,-8.867929 +99213672.530,99.213673,-8.867974,-8.878883 +99213965.499,99.213965,-9.002869,-8.892468 +99214258.467,99.214258,-9.149342,-8.910049 +99214551.436,99.214551,-9.167730,-8.923402 +99214844.404,99.214844,-8.456731,-8.938415 +99215137.373,99.215137,-8.513646,-8.954690 +99215430.341,99.215430,-8.523416,-8.972491 +99215723.310,99.215723,-8.654363,-8.988159 +99216016.278,99.216016,-8.820373,-9.002327 +99216309.247,99.216309,-9.136834,-9.018602 +99216602.215,99.216602,-9.247039,-9.035059 +99216895.184,99.216895,-9.187562,-9.053769 +99217188.152,99.217188,-8.705950,-9.071606 +99217481.121,99.217481,-9.272386,-9.089800 +99217774.089,99.217774,-8.650527,-9.111742 +99218067.058,99.218067,-9.087653,-9.133709 +99218360.026,99.218360,-9.128379,-9.151322 +99218652.995,99.218653,-8.668302,-9.167217 +99218945.963,99.218946,-8.848994,-9.188506 +99219238.932,99.219239,-8.605388,-9.209146 +99219531.900,99.219532,-8.642262,-9.225696 +99219824.869,99.219825,-9.193764,-9.246358 +99220117.837,99.220118,-9.449802,-9.268084 +99220410.806,99.220411,-9.122321,-9.287425 +99220703.774,99.220704,-8.922849,-9.310901 +99220996.743,99.220997,-9.171256,-9.337296 +99221289.711,99.221290,-9.384909,-9.363541 +99221582.680,99.221583,-9.355171,-9.391293 +99221875.648,99.221876,-9.853123,-9.419556 +99222168.617,99.222169,-9.944978,-9.444891 +99222461.585,99.222462,-9.657708,-9.470330 +99222754.554,99.222755,-9.351137,-9.491115 +99223047.522,99.223048,-9.709667,-9.511981 +99223340.491,99.223340,-9.877025,-9.537609 +99223633.459,99.223633,-9.642329,-9.567369 +99223926.428,99.223926,-9.906746,-9.595398 +99224219.396,99.224219,-9.925674,-9.619613 +99224512.365,99.224512,-9.690228,-9.646340 +99224805.333,99.224805,-9.763567,-9.677758 +99225098.302,99.225098,-10.109529,-9.705924 +99225391.271,99.225391,-10.017727,-9.732471 +99225684.239,99.225684,-10.173066,-9.760529 +99225977.208,99.225977,-9.995728,-9.787667 +99226270.176,99.226270,-10.025180,-9.816711 +99226563.145,99.226563,-10.255099,-9.850533 +99226856.113,99.226856,-10.184788,-9.883985 +99227149.082,99.227149,-9.608241,-9.914559 +99227442.050,99.227442,-9.592606,-9.944976 +99227735.019,99.227735,-9.493028,-9.971339 +99228027.987,99.228028,-9.734618,-10.002387 +99228320.956,99.228321,-9.704950,-10.037392 +99228613.924,99.228614,-9.805281,-10.067027 +99228906.893,99.228907,-10.011662,-10.093006 +99229199.861,99.229200,-10.578741,-10.125270 +99229492.830,99.229493,-10.287686,-10.155464 +99229785.798,99.229786,-10.406082,-10.185625 +99230078.767,99.230079,-10.688928,-10.216978 +99230371.735,99.230372,-10.270369,-10.246854 +99230664.704,99.230665,-10.256433,-10.279413 +99230957.672,99.230958,-10.455380,-10.314458 +99231250.641,99.231251,-10.085155,-10.347424 +99231543.609,99.231544,-9.886001,-10.376289 +99231836.578,99.231837,-10.288659,-10.412220 +99232129.546,99.232130,-10.768185,-10.449184 +99232422.515,99.232423,-10.777817,-10.485562 +99232715.483,99.232715,-10.724216,-10.522677 +99233008.452,99.233008,-10.538751,-10.562823 +99233301.420,99.233301,-10.481034,-10.596280 +99233594.389,99.233594,-10.645465,-10.629100 +99233887.357,99.233887,-10.895266,-10.662564 +99234180.326,99.234180,-10.856268,-10.695277 +99234473.294,99.234473,-10.670779,-10.725621 +99234766.263,99.234766,-10.711601,-10.754673 +99235059.231,99.235059,-10.887722,-10.792136 +99235352.200,99.235352,-10.912560,-10.830536 +99235645.168,99.235645,-10.441425,-10.867871 +99235938.137,99.235938,-10.695875,-10.904710 +99236231.105,99.236231,-10.738984,-10.940444 +99236524.074,99.236524,-10.660276,-10.975559 +99236817.042,99.236817,-10.948845,-11.009753 +99237110.011,99.237110,-10.877874,-11.044367 +99237402.979,99.237403,-11.031027,-11.084370 +99237695.948,99.237696,-11.281091,-11.116866 +99237988.916,99.237989,-11.074507,-11.157580 +99238281.885,99.238282,-10.687886,-11.194442 +99238574.853,99.238575,-11.059659,-11.231959 +99238867.822,99.238868,-11.453269,-11.276306 +99239160.790,99.239161,-11.435314,-11.317295 +99239453.759,99.239454,-11.543604,-11.360612 +99239746.727,99.239747,-11.638210,-11.404034 +99240039.696,99.240040,-11.787879,-11.441805 +99240332.664,99.240333,-11.682938,-11.477601 +99240625.633,99.240626,-11.952229,-11.518127 +99240918.601,99.240919,-12.107719,-11.562106 +99241211.570,99.241212,-11.744259,-11.602639 +99241504.538,99.241505,-12.000828,-11.640000 +99241797.507,99.241798,-12.396243,-11.676728 +99242090.475,99.242090,-12.529579,-11.709286 +99242383.444,99.242383,-12.043655,-11.742526 +99242676.412,99.242676,-12.004551,-11.780392 +99242969.381,99.242969,-12.186666,-11.822931 +99243262.349,99.243262,-12.493651,-11.861268 +99243555.318,99.243555,-12.754479,-11.898262 +99243848.286,99.243848,-12.307727,-11.938985 +99244141.255,99.244141,-12.045153,-11.978751 +99244434.223,99.244434,-11.959250,-12.019374 +99244727.192,99.244727,-12.214951,-12.063443 +99245020.160,99.245020,-12.336220,-12.107311 +99245313.129,99.245313,-12.404907,-12.148351 +99245606.097,99.245606,-12.401682,-12.191053 +99245899.066,99.245899,-12.533982,-12.232883 +99246192.034,99.246192,-12.426193,-12.278074 +99246485.003,99.246485,-12.386858,-12.323593 +99246777.971,99.246778,-12.214706,-12.366525 +99247070.940,99.247071,-12.188560,-12.411300 +99247363.908,99.247364,-12.457282,-12.464587 +99247656.877,99.247657,-12.660487,-12.518799 +99247949.845,99.247950,-12.598545,-12.574699 +99248242.814,99.248243,-12.747177,-12.628356 +99248535.782,99.248536,-12.569606,-12.683589 +99248828.751,99.248829,-12.527485,-12.739061 +99249121.719,99.249122,-12.967073,-12.792979 +99249414.688,99.249415,-13.115651,-12.840241 +99249707.656,99.249708,-13.348757,-12.891644 +99250000.625,99.250001,-13.517882,-12.942016 +99250293.594,99.250294,-13.315260,-12.989307 +99250586.562,99.250587,-13.062331,-13.042794 +99250879.531,99.250880,-13.234898,-13.097992 +99251172.499,99.251172,-13.384438,-13.151559 +99251465.468,99.251465,-13.069277,-13.211393 +99251758.436,99.251758,-12.933252,-13.275196 +99252051.405,99.252051,-13.177312,-13.333911 +99252344.373,99.252344,-13.679854,-13.386806 +99252637.342,99.252637,-13.794571,-13.440204 +99252930.310,99.252930,-14.058991,-13.495095 +99253223.279,99.253223,-14.383780,-13.552749 +99253516.247,99.253516,-14.162716,-13.612118 +99253809.216,99.253809,-14.111557,-13.670444 +99254102.184,99.254102,-14.254172,-13.726164 +99254395.153,99.254395,-13.940392,-13.783112 +99254688.121,99.254688,-13.581565,-13.843475 +99254981.090,99.254981,-13.588320,-13.904052 +99255274.058,99.255274,-13.945026,-13.963089 +99255567.027,99.255567,-13.601480,-14.022831 +99255859.995,99.255860,-13.812967,-14.091086 +99256152.964,99.256153,-13.973116,-14.155822 +99256445.932,99.256446,-14.796105,-14.221327 +99256738.901,99.256739,-14.801274,-14.289421 +99257031.869,99.257032,-14.637425,-14.353800 +99257324.838,99.257325,-14.443051,-14.419708 +99257617.806,99.257618,-14.125354,-14.484218 +99257910.775,99.257911,-14.248686,-14.546305 +99258203.743,99.258204,-14.599234,-14.612549 +99258496.712,99.258497,-14.851940,-14.686497 +99258789.680,99.258790,-15.075329,-14.755089 +99259082.649,99.259083,-14.631703,-14.818338 +99259375.617,99.259376,-14.262514,-14.882980 +99259668.586,99.259669,-14.310163,-14.946413 +99259961.554,99.259962,-14.617396,-15.009443 +99260254.523,99.260255,-14.838582,-15.071741 +99260547.491,99.260547,-14.969321,-15.136508 +99260840.460,99.260840,-15.109585,-15.197934 +99261133.428,99.261133,-14.920643,-15.257900 +99261426.397,99.261426,-14.675286,-15.324725 +99261719.365,99.261719,-14.327250,-15.388541 +99262012.334,99.262012,-14.699443,-15.447296 +99262305.302,99.262305,-15.113693,-15.505188 +99262598.271,99.262598,-15.439893,-15.571763 +99262891.239,99.262891,-15.608464,-15.639984 +99263184.208,99.263184,-15.562135,-15.705601 +99263477.176,99.263477,-15.658757,-15.767237 +99263770.145,99.263770,-15.850756,-15.826115 +99264063.113,99.264063,-16.523308,-15.893233 +99264356.082,99.264356,-16.808184,-15.966400 +99264649.050,99.264649,-16.880074,-16.042213 +99264942.019,99.264942,-16.994066,-16.114735 +99265234.987,99.265235,-17.400934,-16.186191 +99265527.956,99.265528,-17.482086,-16.257786 +99265820.924,99.265821,-17.746742,-16.330854 +99266113.893,99.266114,-18.156160,-16.403162 +99266406.861,99.266407,-18.272257,-16.478588 +99266699.830,99.266700,-18.133078,-16.555757 +99266992.798,99.266993,-18.432039,-16.637550 +99267285.767,99.267286,-18.680429,-16.722150 +99267578.735,99.267579,-18.702505,-16.803049 +99267871.704,99.267872,-18.452293,-16.881156 +99268164.672,99.268165,-18.756319,-16.962187 +99268457.641,99.268458,-19.028940,-17.041869 +99268750.609,99.268751,-19.643375,-17.126712 +99269043.578,99.269044,-20.102556,-17.214977 +99269336.546,99.269337,-20.186577,-17.296358 +99269629.515,99.269630,-20.099951,-17.376057 +99269922.483,99.269922,-20.045622,-17.452594 +99270215.452,99.270215,-20.013521,-17.527530 +99270508.420,99.270508,-20.127390,-17.608256 +99270801.389,99.270801,-20.756800,-17.695613 +99271094.357,99.271094,-21.105394,-17.781001 +99271387.326,99.271387,-21.348724,-17.864613 +99271680.294,99.271680,-21.377468,-17.956365 +99271973.263,99.271973,-21.532936,-18.053691 +99272266.231,99.272266,-21.662884,-18.147499 +99272559.200,99.272559,-21.645723,-18.232190 +99272852.168,99.272852,-21.729502,-18.316548 +99273145.137,99.273145,-21.226271,-18.397454 +99273438.105,99.273438,-21.289248,-18.473587 +99273731.074,99.273731,-21.782383,-18.555325 +99274024.042,99.274024,-21.964254,-18.639870 +99274317.011,99.274317,-21.918352,-18.723141 +99274609.979,99.274610,-21.836382,-18.814874 +99274902.948,99.274903,-21.826799,-18.916950 +99275195.917,99.275196,-22.055628,-19.021224 +99275488.885,99.275489,-22.292200,-19.119681 +99275781.854,99.275782,-22.415695,-19.229152 +99276074.822,99.276075,-22.094210,-19.336104 +99276367.791,99.276368,-22.531006,-19.441670 +99276660.759,99.276661,-22.787294,-19.529957 +99276953.728,99.276954,-22.761930,-19.620417 +99277246.696,99.277247,-22.233318,-19.716430 +99277539.665,99.277540,-22.321789,-19.818862 +99277832.633,99.277833,-22.835770,-19.932787 +99278125.602,99.278126,-22.773542,-20.046414 +99278418.570,99.278419,-22.689451,-20.153476 +99278711.539,99.278712,-22.691530,-20.256539 +99279004.507,99.279005,-22.765678,-20.356670 +99279297.476,99.279297,-22.911007,-20.470866 +99279590.444,99.279590,-22.553816,-20.598969 +99279883.413,99.279883,-22.692129,-20.729736 +99280176.381,99.280176,-23.291351,-20.855132 +99280469.350,99.280469,-23.191568,-20.977787 +99280762.318,99.280762,-23.159012,-21.100188 +99281055.287,99.281055,-23.197775,-21.221597 +99281348.255,99.281348,-23.385798,-21.351974 +99281641.224,99.281641,-23.524458,-21.494939 +99281934.192,99.281934,-23.605753,-21.655912 +99282227.161,99.282227,-23.872999,-21.808064 +99282520.129,99.282520,-24.205799,-21.951208 +99282813.098,99.282813,-24.160862,-22.087834 +99283106.066,99.283106,-23.586761,-22.222924 +99283399.035,99.283399,-23.545443,-22.363575 +99283692.003,99.283692,-23.826437,-22.505664 +99283984.972,99.283985,-24.146099,-22.647154 +99284277.940,99.284278,-24.533028,-22.771604 +99284570.909,99.284571,-24.346432,-22.890074 +99284863.877,99.284864,-24.422392,-23.008859 +99285156.846,99.285157,-24.315836,-23.125617 +99285449.814,99.285450,-24.467405,-23.232435 +99285742.783,99.285743,-24.664076,-23.343545 +99286035.751,99.286036,-25.026676,-23.450884 +99286328.720,99.286329,-24.948175,-23.549636 +99286621.688,99.286622,-24.641184,-23.647939 +99286914.657,99.286915,-24.485867,-23.752272 +99287207.625,99.287208,-25.098249,-23.851660 +99287500.594,99.287501,-25.215187,-23.946893 +99287793.562,99.287794,-24.932985,-24.042926 +99288086.531,99.288087,-25.168680,-24.147856 +99288379.499,99.288379,-25.240072,-24.247012 +99288672.468,99.288672,-25.280678,-24.342310 +99288965.436,99.288965,-26.079248,-24.425249 +99289258.405,99.289258,-26.565468,-24.500442 +99289551.373,99.289551,-26.244423,-24.575328 +99289844.342,99.289844,-26.089434,-24.653718 +99290137.310,99.290137,-26.286448,-24.734026 +99290430.279,99.290430,-26.931700,-24.815949 +99290723.247,99.290723,-26.810364,-24.895911 +99291016.216,99.291016,-26.702663,-24.963241 +99291309.184,99.291309,-26.328278,-25.027140 +99291602.153,99.291602,-26.025791,-25.088332 +99291895.121,99.291895,-26.400393,-25.150739 +99292188.090,99.292188,-26.408215,-25.212462 +99292481.058,99.292481,-26.337156,-25.271704 +99292774.027,99.292774,-26.685404,-25.332857 +99293066.995,99.293067,-26.956539,-25.393773 +99293359.964,99.293360,-26.964563,-25.462803 +99293652.932,99.293653,-26.892210,-25.532588 +99293945.901,99.293946,-27.066006,-25.595109 +99294238.869,99.294239,-27.075367,-25.655546 +99294531.838,99.294532,-27.121977,-25.717455 +99294824.806,99.294825,-26.930243,-25.782126 +99295117.775,99.295118,-26.858328,-25.847999 +99295410.743,99.295411,-27.338593,-25.909222 +99295703.712,99.295704,-27.275843,-25.967983 +99295996.680,99.295997,-27.535482,-26.025427 +99296289.649,99.296290,-27.635117,-26.089159 +99296582.617,99.296583,-27.467936,-26.145015 +99296875.586,99.296876,-27.866575,-26.197995 +99297168.554,99.297169,-27.734291,-26.252006 +99297461.523,99.297462,-27.303619,-26.315590 +99297754.491,99.297754,-27.705137,-26.376564 +99298047.460,99.298047,-27.787622,-26.431318 +99298340.428,99.298340,-27.762281,-26.488701 +99298633.397,99.298633,-27.680052,-26.549847 +99298926.365,99.298926,-27.838860,-26.610098 +99299219.334,99.299219,-27.771608,-26.669470 +99299512.302,99.299512,-27.577564,-26.729073 +99299805.271,99.299805,-27.784542,-26.794922 +99300098.240,99.300098,-28.027592,-26.860287 +99300391.208,99.300391,-28.273661,-26.917606 +99300684.177,99.300684,-28.387514,-26.975045 +99300977.145,99.300977,-28.186646,-27.031732 +99301270.114,99.301270,-27.896147,-27.088169 +99301563.082,99.301563,-28.010496,-27.142435 +99301856.051,99.301856,-27.988892,-27.194024 +99302149.019,99.302149,-27.758942,-27.246843 +99302441.988,99.302442,-28.203264,-27.295138 +99302734.956,99.302735,-28.228542,-27.338299 +99303027.925,99.303028,-28.111767,-27.382277 +99303320.893,99.303321,-27.917410,-27.439760 +99303613.862,99.303614,-28.005123,-27.499496 +99303906.830,99.303907,-28.746733,-27.551219 +99304199.799,99.304200,-29.104288,-27.598620 +99304492.767,99.304493,-28.604967,-27.643908 +99304785.736,99.304786,-28.207632,-27.692196 +99305078.704,99.305079,-27.439390,-27.740347 +99305371.673,99.305372,-27.018997,-27.789297 +99305664.641,99.305665,-28.488272,-27.836876 +99305957.610,99.305958,-28.817493,-27.881356 +99306250.578,99.306251,-28.617754,-27.920573 +99306543.547,99.306544,-28.711287,-27.964703 +99306836.515,99.306837,-28.758471,-28.009925 +99307129.484,99.307129,-28.914202,-28.055641 +99307422.452,99.307422,-28.827745,-28.095493 +99307715.421,99.307715,-28.516411,-28.134480 +99308008.389,99.308008,-28.570606,-28.176383 +99308301.358,99.308301,-28.487722,-28.216060 +99308594.326,99.308594,-28.785973,-28.256369 +99308887.295,99.308887,-28.791815,-28.296434 +99309180.263,99.309180,-28.934626,-28.325893 +99309473.232,99.309473,-28.960608,-28.350479 +99309766.200,99.309766,-29.112413,-28.379388 +99310059.169,99.310059,-28.863565,-28.411038 +99310352.137,99.310352,-28.658440,-28.438921 +99310645.106,99.310645,-28.341173,-28.460034 +99310938.074,99.310938,-28.028843,-28.483203 +99311231.043,99.311231,-28.628103,-28.506913 +99311524.011,99.311524,-28.838341,-28.534525 +99311816.980,99.311817,-29.116560,-28.562979 +99312109.948,99.312110,-29.504278,-28.587027 +99312402.917,99.312403,-29.102701,-28.611789 +99312695.885,99.312696,-29.371864,-28.638076 +99312988.854,99.312989,-29.484533,-28.659899 +99313281.822,99.313282,-28.858929,-28.675166 +99313574.791,99.313575,-29.123013,-28.695713 +99313867.759,99.313868,-29.369455,-28.721043 +99314160.728,99.314161,-29.425968,-28.741777 +99314453.696,99.314454,-29.368597,-28.760047 +99314746.665,99.314747,-29.494654,-28.778946 +99315039.633,99.315040,-29.509813,-28.800277 +99315332.602,99.315333,-29.064066,-28.824827 +99315625.570,99.315626,-29.293638,-28.844066 +99315918.539,99.315919,-29.287300,-28.863825 +99316211.507,99.316212,-29.243486,-28.877857 +99316504.476,99.316504,-28.944395,-28.891602 +99316797.444,99.316797,-29.108137,-28.906834 +99317090.413,99.317090,-29.098896,-28.914346 +99317383.381,99.317383,-28.996614,-28.926120 +99317676.350,99.317676,-28.546515,-28.944583 +99317969.318,99.317969,-29.024899,-28.955287 +99318262.287,99.318262,-29.243868,-28.968462 +99318555.255,99.318555,-29.648890,-28.983711 +99318848.224,99.318848,-29.195753,-29.000989 +99319141.192,99.319141,-29.102615,-29.014963 +99319434.161,99.319434,-29.605247,-29.029656 +99319727.129,99.319727,-29.296061,-29.049999 +99320020.098,99.320020,-29.588892,-29.068429 +99320313.066,99.320313,-30.063593,-29.083109 +99320606.035,99.320606,-29.448679,-29.092147 +99320899.003,99.320899,-28.973997,-29.101042 +99321191.972,99.321192,-28.910439,-29.112429 +99321484.940,99.321485,-28.997025,-29.125569 +99321777.909,99.321778,-28.800890,-29.137624 +99322070.877,99.322071,-29.210436,-29.148784 +99322363.846,99.322364,-29.054050,-29.161036 +99322656.814,99.322657,-28.957689,-29.172355 +99322949.783,99.322950,-28.898764,-29.184225 +99323242.751,99.323243,-29.637264,-29.198125 +99323535.720,99.323536,-29.805536,-29.213806 +99323828.688,99.323829,-29.000465,-29.227599 +99324121.657,99.324122,-29.080460,-29.235214 +99324414.625,99.324415,-29.785269,-29.240233 +99324707.594,99.324708,-29.706604,-29.247909 +99325000.562,99.325001,-29.820625,-29.259109 +99325293.531,99.325294,-29.514315,-29.277925 +99325586.500,99.325586,-29.619694,-29.299896 +99325879.468,99.325879,-29.510159,-29.309817 +99326172.437,99.326172,-29.442434,-29.319197 +99326465.405,99.326465,-30.156645,-29.328153 +99326758.374,99.326758,-29.329063,-29.336027 +99327051.342,99.327051,-28.889757,-29.344006 +99327344.311,99.327344,-29.456060,-29.347947 +99327637.279,99.327637,-29.542294,-29.351384 +99327930.248,99.327930,-29.244070,-29.358309 +99328223.216,99.328223,-29.409180,-29.365286 +99328516.185,99.328516,-29.651272,-29.371864 +99328809.153,99.328809,-29.646687,-29.378157 +99329102.122,99.329102,-29.603989,-29.385674 +99329395.090,99.329395,-29.713806,-29.390222 +99329688.059,99.329688,-29.806547,-29.390677 +99329981.027,99.329981,-29.931242,-29.391777 +99330273.996,99.330274,-29.635752,-29.397400 +99330566.964,99.330567,-29.685591,-29.404375 +99330859.933,99.330860,-29.808172,-29.416098 +99331152.901,99.331153,-29.670013,-29.431570 +99331445.870,99.331446,-29.568285,-29.440450 +99331738.838,99.331739,-29.054331,-29.440648 +99332031.807,99.332032,-29.074745,-29.442395 +99332324.775,99.332325,-29.178745,-29.445819 +99332617.744,99.332618,-29.258654,-29.450302 +99332910.712,99.332911,-29.188175,-29.451402 +99333203.681,99.333204,-28.662195,-29.452975 +99333496.649,99.333497,-29.452551,-29.458457 +99333789.618,99.333790,-30.134169,-29.464001 +99334082.586,99.334083,-29.628796,-29.467297 +99334375.555,99.334376,-29.235331,-29.469209 +99334668.523,99.334669,-29.398460,-29.470363 +99334961.492,99.334961,-29.437056,-29.471837 +99335254.460,99.335254,-29.815233,-29.473056 +99335547.429,99.335547,-29.780315,-29.479196 +99335840.397,99.335840,-29.743118,-29.482827 +99336133.366,99.336133,-29.245567,-29.485158 +99336426.334,99.336426,-29.343248,-29.487258 +99336719.303,99.336719,-29.309397,-29.492256 +99337012.271,99.337012,-28.762075,-29.496413 +99337305.240,99.337305,-29.177029,-29.502947 +99337598.208,99.337598,-29.522137,-29.509782 +99337891.177,99.337891,-28.975023,-29.517775 +99338184.145,99.338184,-29.444235,-29.521950 +99338477.114,99.338477,-29.725973,-29.522884 +99338770.082,99.338770,-29.919699,-29.521917 +99339063.051,99.339063,-29.622244,-29.526670 +99339356.019,99.339356,-29.625980,-29.534396 +99339648.988,99.339649,-30.239700,-29.536471 +99339941.956,99.339942,-30.252602,-29.536822 +99340234.925,99.340235,-29.991480,-29.537378 +99340527.893,99.340528,-29.444878,-29.534947 +99340820.862,99.340821,-29.569929,-29.536908 +99341113.830,99.341114,-29.677830,-29.545699 +99341406.799,99.341407,-29.525232,-29.554352 +99341699.767,99.341700,-29.520943,-29.559239 +99341992.736,99.341993,-29.355047,-29.567360 +99342285.704,99.342286,-29.187088,-29.569540 +99342578.673,99.342579,-29.666410,-29.572640 +99342871.641,99.342872,-29.783096,-29.580599 +99343164.610,99.343165,-29.929401,-29.587942 +99343457.578,99.343458,-29.907957,-29.585782 +99343750.547,99.343751,-29.737211,-29.582716 +99344043.515,99.344044,-29.797243,-29.585200 +99344336.484,99.344336,-29.828070,-29.585691 +99344629.452,99.344629,-29.623354,-29.586719 +99344922.421,99.344922,-29.620392,-29.586994 +99345215.389,99.345215,-29.582731,-29.585356 +99345508.358,99.345508,-29.317587,-29.583728 +99345801.326,99.345801,-29.791906,-29.583456 +99346094.295,99.346094,-30.148109,-29.582891 +99346387.263,99.346387,-29.810385,-29.581989 +99346680.232,99.346680,-29.764328,-29.571840 +99346973.200,99.346973,-29.838480,-29.569272 +99347266.169,99.347266,-29.432774,-29.571622 +99347559.137,99.347559,-29.266680,-29.570566 +99347852.106,99.347852,-29.378815,-29.565870 +99348145.074,99.348145,-29.450701,-29.564085 +99348438.043,99.348438,-29.293898,-29.559661 +99348731.011,99.348731,-29.612717,-29.552507 +99349023.980,99.349024,-29.799028,-29.538340 +99349316.948,99.349317,-29.535753,-29.528288 +99349609.917,99.349610,-29.017378,-29.522786 +99349902.885,99.349903,-29.256128,-29.520858 +99350195.854,99.350196,-29.605049,-29.518780 +99350488.823,99.350489,-29.549274,-29.519487 +99350781.791,99.350782,-29.823154,-29.519783 +99351074.760,99.351075,-29.926464,-29.516331 +99351367.728,99.351368,-29.777674,-29.511447 +99351660.697,99.351661,-28.862059,-29.509636 +99351953.665,99.351954,-29.344587,-29.509284 +99352246.634,99.352247,-30.007887,-29.507436 +99352539.602,99.352540,-29.710678,-29.508688 +99352832.571,99.352833,-29.522314,-29.507610 +99353125.539,99.353126,-29.707296,-29.505072 +99353418.508,99.353419,-29.568472,-29.509892 +99353711.476,99.353711,-29.889471,-29.510465 +99354004.445,99.354004,-29.835207,-29.504240 +99354297.413,99.354297,-29.693481,-29.502324 +99354590.382,99.354590,-29.525858,-29.501676 +99354883.350,99.354883,-29.702532,-29.500718 +99355176.319,99.355176,-29.681658,-29.501402 +99355469.287,99.355469,-29.905569,-29.498802 +99355762.256,99.355762,-29.798336,-29.497660 +99356055.224,99.356055,-29.603765,-29.494655 +99356348.193,99.356348,-29.524408,-29.495454 +99356641.161,99.356641,-29.594759,-29.497206 +99356934.130,99.356934,-29.663122,-29.498279 +99357227.098,99.357227,-30.003092,-29.503068 +99357520.067,99.357520,-29.920982,-29.504985 +99357813.035,99.357813,-29.525452,-29.507823 +99358106.004,99.358106,-29.567831,-29.513965 +99358398.972,99.358399,-29.365597,-29.516471 +99358691.941,99.358692,-29.514563,-29.515781 +99358984.909,99.358985,-29.845802,-29.514289 +99359277.878,99.359278,-30.186392,-29.513674 +99359570.846,99.359571,-29.904139,-29.512670 +99359863.815,99.359864,-29.341854,-29.508894 +99360156.783,99.360157,-29.666586,-29.503678 +99360449.752,99.360450,-29.703075,-29.499475 +99360742.720,99.360743,-29.720213,-29.503347 +99361035.689,99.361036,-30.185171,-29.505140 +99361328.657,99.361329,-30.077019,-29.506127 +99361621.626,99.361622,-29.629515,-29.508881 +99361914.594,99.361915,-29.851868,-29.508964 +99362207.563,99.362208,-29.494596,-29.507734 +99362500.531,99.362501,-29.448414,-29.508032 +99362793.500,99.362793,-30.024111,-29.505259 +99363086.468,99.363086,-29.855911,-29.502249 +99363379.437,99.363379,-29.347868,-29.500051 +99363672.405,99.363672,-29.384588,-29.499430 +99363965.374,99.363965,-29.308626,-29.501367 +99364258.342,99.364258,-29.140757,-29.502553 +99364551.311,99.364551,-29.935339,-29.500892 +99364844.279,99.364844,-29.745516,-29.500899 +99365137.248,99.365137,-29.589911,-29.504362 +99365430.216,99.365430,-29.300253,-29.503508 +99365723.185,99.365723,-29.582251,-29.508264 +99366016.153,99.366016,-29.434700,-29.508525 +99366309.122,99.366309,-29.324476,-29.505682 +99366602.090,99.366602,-28.802553,-29.505522 +99366895.059,99.366895,-29.008368,-29.505959 +99367188.027,99.367188,-29.174072,-29.504235 +99367480.996,99.367481,-29.317465,-29.503952 +99367773.964,99.367774,-28.945679,-29.504423 +99368066.933,99.368067,-29.022722,-29.506310 +99368359.901,99.368360,-28.860147,-29.506988 +99368652.870,99.368653,-28.749840,-29.508447 +99368945.838,99.368946,-28.010792,-29.507223 +99369238.807,99.369239,-28.390699,-29.501773 +99369531.775,99.369532,-28.990421,-29.499597 +99369824.744,99.369825,-29.533236,-29.502435 +99370117.712,99.370118,-29.629023,-29.504653 +99370410.681,99.370411,-29.736427,-29.503592 +99370703.649,99.370704,-29.727850,-29.501080 +99370996.618,99.370997,-29.329931,-29.498632 +99371289.586,99.371290,-29.026512,-29.497817 +99371582.555,99.371583,-29.323845,-29.496912 +99371875.523,99.371876,-29.011213,-29.502267 +99372168.492,99.372168,-28.851418,-29.505001 +99372461.460,99.372461,-29.340788,-29.505053 +99372754.429,99.372754,-29.121359,-29.508462 +99373047.397,99.373047,-28.876450,-29.513151 +99373340.366,99.373340,-29.242922,-29.511132 +99373633.334,99.373633,-29.530718,-29.508436 +99373926.303,99.373926,-29.246222,-29.507064 +99374219.271,99.374219,-29.366650,-29.503956 +99374512.240,99.374512,-29.152660,-29.504587 +99374805.208,99.374805,-29.272177,-29.507750 +99375098.177,99.375098,-29.530334,-29.510313 +99375391.146,99.375391,-29.448153,-29.511797 +99375684.114,99.375684,-29.616537,-29.509479 +99375977.083,99.375977,-29.327873,-29.509247 +99376270.051,99.376270,-29.350128,-29.513764 +99376563.020,99.376563,-29.581398,-29.519585 +99376855.988,99.376856,-29.452488,-29.521983 +99377148.957,99.377149,-29.354183,-29.520061 +99377441.925,99.377442,-29.427599,-29.512808 +99377734.894,99.377735,-29.931107,-29.507239 +99378027.862,99.378028,-29.790770,-29.501024 +99378320.831,99.378321,-29.795918,-29.500142 +99378613.799,99.378614,-29.627836,-29.502133 +99378906.768,99.378907,-29.701004,-29.500489 +99379199.736,99.379200,-29.536654,-29.498669 +99379492.705,99.379493,-29.487057,-29.498608 +99379785.673,99.379786,-29.667372,-29.498533 +99380078.642,99.380079,-29.477510,-29.497491 +99380371.610,99.380372,-29.390375,-29.493530 +99380664.579,99.380665,-30.002872,-29.489545 +99380957.547,99.380958,-29.826715,-29.488129 +99381250.516,99.381251,-29.820929,-29.485726 +99381543.484,99.381543,-29.921631,-29.482756 +99381836.453,99.381836,-29.532274,-29.479632 +99382129.421,99.382129,-29.195353,-29.476537 +99382422.390,99.382422,-29.225286,-29.479341 +99382715.358,99.382715,-29.289072,-29.483115 +99383008.327,99.383008,-29.364235,-29.483202 +99383301.295,99.383301,-29.609177,-29.487322 +99383594.264,99.383594,-29.815519,-29.492840 +99383887.232,99.383887,-30.026886,-29.494645 +99384180.201,99.384180,-29.974613,-29.497369 +99384473.169,99.384473,-29.589539,-29.505440 +99384766.138,99.384766,-29.624329,-29.503946 +99385059.106,99.385059,-30.137403,-29.501610 +99385352.075,99.385352,-29.465145,-29.502380 +99385645.043,99.385645,-29.991278,-29.505405 +99385938.012,99.385938,-29.830114,-29.503826 +99386230.980,99.386231,-29.718687,-29.503841 +99386523.949,99.386524,-29.786970,-29.507748 +99386816.917,99.386817,-29.828287,-29.518300 +99387109.886,99.387110,-29.590675,-29.523214 +99387402.854,99.387403,-29.394754,-29.525113 +99387695.823,99.387696,-29.328302,-29.527250 +99387988.791,99.387989,-29.637405,-29.531033 +99388281.760,99.388282,-29.543406,-29.534852 +99388574.728,99.388575,-29.488430,-29.540687 +99388867.697,99.388868,-29.444197,-29.543149 +99389160.665,99.389161,-29.065668,-29.556550 +99389453.634,99.389454,-29.245148,-29.569194 +99389746.602,99.389747,-29.379930,-29.573334 +99390039.571,99.390040,-29.553082,-29.573918 +99390332.539,99.390333,-29.458742,-29.574407 +99390625.508,99.390626,-29.214685,-29.573738 +99390918.476,99.390918,-29.476103,-29.575154 +99391211.445,99.391211,-29.804930,-29.577913 +99391504.413,99.391504,-29.647425,-29.580795 +99391797.382,99.391797,-29.546682,-29.579679 +99392090.350,99.392090,-29.721613,-29.582944 +99392383.319,99.392383,-30.015856,-29.583271 +99392676.287,99.392676,-30.230104,-29.583413 +99392969.256,99.392969,-30.219362,-29.586397 +99393262.224,99.393262,-29.427240,-29.595545 +99393555.193,99.393555,-29.209341,-29.599631 +99393848.161,99.393848,-29.689009,-29.602707 +99394141.130,99.394141,-29.398508,-29.605228 +99394434.098,99.394434,-29.784740,-29.604773 +99394727.067,99.394727,-29.984531,-29.606795 +99395020.035,99.395020,-30.086119,-29.612326 +99395313.004,99.395313,-29.898388,-29.616755 +99395605.972,99.395606,-29.571085,-29.619818 +99395898.941,99.395899,-29.764503,-29.619490 +99396191.909,99.396192,-30.286839,-29.621090 +99396484.878,99.396485,-30.407187,-29.620700 +99396777.846,99.396778,-29.942612,-29.618648 +99397070.815,99.397071,-29.399120,-29.617018 +99397363.783,99.397364,-29.013493,-29.617028 +99397656.752,99.397657,-29.154135,-29.616393 +99397949.720,99.397950,-28.744453,-29.612979 +99398242.689,99.398243,-29.446795,-29.609451 +99398535.657,99.398536,-29.638388,-29.608639 +99398828.626,99.398829,-29.294300,-29.610009 +99399121.594,99.399122,-29.583933,-29.610003 +99399414.563,99.399415,-30.176567,-29.613270 +99399707.531,99.399708,-29.892914,-29.618685 +99400000.500,99.400001,-29.206301,-29.623135 +99400293.469,99.400293,-29.135817,-29.626682 +99400586.437,99.400586,-29.164696,-29.626993 +99400879.406,99.400879,-29.520243,-29.622260 +99401172.374,99.401172,-29.814844,-29.619264 +99401465.343,99.401465,-29.634073,-29.615558 +99401758.311,99.401758,-29.207737,-29.611701 +99402051.280,99.402051,-29.412731,-29.610288 +99402344.248,99.402344,-29.898504,-29.608198 +99402637.217,99.402637,-29.994444,-29.610827 +99402930.185,99.402930,-30.037539,-29.612143 +99403223.154,99.403223,-30.518736,-29.614035 +99403516.122,99.403516,-30.149082,-29.611890 +99403809.091,99.403809,-29.632662,-29.607753 +99404102.059,99.404102,-29.681738,-29.604631 +99404395.028,99.404395,-30.300179,-29.603673 +99404687.996,99.404688,-29.715075,-29.605864 +99404980.965,99.404981,-29.419594,-29.606629 +99405273.933,99.405274,-29.698898,-29.607665 +99405566.902,99.405567,-29.714907,-29.612371 +99405859.870,99.405860,-29.367373,-29.610491 +99406152.839,99.406153,-29.436762,-29.609688 +99406445.807,99.406446,-29.870731,-29.611020 +99406738.776,99.406739,-30.245161,-29.612591 +99407031.744,99.407032,-29.652071,-29.615004 +99407324.713,99.407325,-29.421047,-29.612830 +99407617.681,99.407618,-29.605894,-29.604320 +99407910.650,99.407911,-29.424139,-29.598823 +99408203.618,99.408204,-29.514782,-29.593962 +99408496.587,99.408497,-29.604376,-29.589115 +99408789.555,99.408790,-29.040436,-29.592018 +99409082.524,99.409083,-29.538738,-29.593496 +99409375.492,99.409375,-29.967791,-29.598478 +99409668.461,99.409668,-29.517282,-29.603404 +99409961.429,99.409961,-29.613239,-29.609489 +99410254.398,99.410254,-29.697382,-29.612273 +99410547.366,99.410547,-29.642305,-29.615050 +99410840.335,99.410840,-29.933641,-29.619682 +99411133.303,99.411133,-29.702551,-29.622088 +99411426.272,99.411426,-29.388891,-29.620517 +99411719.240,99.411719,-29.182058,-29.617768 +99412012.209,99.412012,-29.422327,-29.615591 +99412305.177,99.412305,-28.889427,-29.613856 +99412598.146,99.412598,-29.359240,-29.609742 +99412891.114,99.412891,-29.505312,-29.607558 +99413184.083,99.413184,-30.095816,-29.602926 +99413477.051,99.413477,-29.792137,-29.604261 +99413770.020,99.413770,-29.966475,-29.610642 +99414062.988,99.414063,-29.576473,-29.611402 +99414355.957,99.414356,-29.308075,-29.614419 +99414648.925,99.414649,-29.409668,-29.614049 +99414941.894,99.414942,-30.036890,-29.612684 +99415234.862,99.415235,-30.170273,-29.609263 +99415527.831,99.415528,-29.871782,-29.607025 +99415820.799,99.415821,-29.571800,-29.609481 +99416113.768,99.416114,-29.537760,-29.608598 +99416406.736,99.416407,-29.300299,-29.605233 +99416699.705,99.416700,-29.311323,-29.601402 +99416992.673,99.416993,-29.242632,-29.600970 +99417285.642,99.417286,-29.355431,-29.602683 +99417578.610,99.417579,-29.345169,-29.606678 +99417871.579,99.417872,-29.455082,-29.611353 +99418164.547,99.418165,-29.314137,-29.621273 +99418457.516,99.418458,-29.681313,-29.621001 +99418750.484,99.418750,-29.820498,-29.615490 +99419043.453,99.419043,-29.700214,-29.615541 +99419336.421,99.419336,-30.000422,-29.615077 +99419629.390,99.419629,-30.274496,-29.611233 +99419922.358,99.419922,-30.331972,-29.611030 +99420215.327,99.420215,-29.974625,-29.612120 +99420508.295,99.420508,-29.430853,-29.616790 +99420801.264,99.420801,-29.347584,-29.620468 +99421094.232,99.421094,-29.416248,-29.621221 +99421387.201,99.421387,-29.319061,-29.620330 +99421680.169,99.421680,-29.388502,-29.620127 +99421973.138,99.421973,-29.346144,-29.622464 +99422266.106,99.422266,-28.942553,-29.622253 +99422559.075,99.422559,-29.568237,-29.620279 +99422852.043,99.422852,-29.459747,-29.618567 +99423145.012,99.423145,-29.616207,-29.615097 +99423437.980,99.423438,-29.325085,-29.607420 +99423730.949,99.423731,-29.258467,-29.604580 +99424023.917,99.424024,-29.579563,-29.602999 +99424316.886,99.424317,-29.833929,-29.601024 +99424609.854,99.424610,-29.899502,-29.593706 +99424902.823,99.424903,-29.730822,-29.588887 +99425195.792,99.425196,-30.300903,-29.590864 +99425488.760,99.425489,-30.138250,-29.592191 +99425781.729,99.425782,-29.718849,-29.588528 +99426074.697,99.426075,-29.715910,-29.587105 +99426367.666,99.426368,-29.909910,-29.591604 +99426660.634,99.426661,-30.017061,-29.589949 +99426953.603,99.426954,-30.190376,-29.582970 +99427246.571,99.427247,-29.304090,-29.583885 +99427539.540,99.427540,-28.406410,-29.587301 +99427832.508,99.427833,-28.673071,-29.589670 +99428125.477,99.428125,-29.012384,-29.590562 +99428418.445,99.428418,-28.931845,-29.590564 +99428711.414,99.428711,-29.895336,-29.589210 +99429004.382,99.429004,-29.644470,-29.591906 +99429297.351,99.429297,-29.716072,-29.591023 +99429590.319,99.429590,-29.916182,-29.592950 +99429883.288,99.429883,-30.253468,-29.602450 +99430176.256,99.430176,-29.946808,-29.607077 +99430469.225,99.430469,-29.842207,-29.606906 +99430762.193,99.430762,-29.835386,-29.604268 +99431055.162,99.431055,-29.807245,-29.599841 +99431348.130,99.431348,-29.586084,-29.598237 +99431641.099,99.431641,-29.283979,-29.602134 +99431934.067,99.431934,-29.262674,-29.607334 +99432227.036,99.432227,-29.484648,-29.608517 +99432520.004,99.432520,-29.437515,-29.612405 +99432812.973,99.432813,-29.897837,-29.611865 +99433105.941,99.433106,-29.543793,-29.612880 +99433398.910,99.433399,-29.606531,-29.606762 +99433691.878,99.433692,-30.090637,-29.599959 +99433984.847,99.433985,-29.796328,-29.596737 +99434277.815,99.434278,-29.810854,-29.596785 +99434570.784,99.434571,-29.732323,-29.598529 +99434863.752,99.434864,-29.785473,-29.599199 +99435156.721,99.435157,-29.592487,-29.594396 +99435449.689,99.435450,-29.582397,-29.588321 +99435742.658,99.435743,-29.918180,-29.586372 +99436035.626,99.436036,-29.640942,-29.585929 +99436328.595,99.436329,-29.778528,-29.583564 +99436621.563,99.436622,-29.817530,-29.582461 +99436914.532,99.436915,-29.879078,-29.584723 +99437207.500,99.437208,-29.629076,-29.588771 +99437500.469,99.437500,-29.519468,-29.593599 +99437793.437,99.437793,-29.773008,-29.595701 +99438086.406,99.438086,-30.028793,-29.598644 +99438379.374,99.438379,-29.411242,-29.601609 +99438672.343,99.438672,-28.939257,-29.599246 +99438965.311,99.438965,-29.300772,-29.592280 +99439258.280,99.439258,-29.521265,-29.589062 +99439551.248,99.439551,-29.614977,-29.584604 +99439844.217,99.439844,-29.863276,-29.580758 +99440137.185,99.440137,-29.344503,-29.576951 +99440430.154,99.440430,-29.750410,-29.574136 +99440723.122,99.440723,-29.643978,-29.574144 +99441016.091,99.441016,-29.622335,-29.575104 +99441309.059,99.441309,-29.688978,-29.573825 +99441602.028,99.441602,-29.606218,-29.574650 +99441894.996,99.441895,-29.509127,-29.576121 +99442187.965,99.442188,-29.385225,-29.576405 +99442480.933,99.442481,-29.619383,-29.579771 +99442773.902,99.442774,-29.745966,-29.578885 +99443066.870,99.443067,-29.543106,-29.579878 +99443359.839,99.443360,-29.384443,-29.574503 +99443652.807,99.443653,-29.729156,-29.574226 +99443945.776,99.443946,-29.419922,-29.575179 +99444238.744,99.444239,-29.414608,-29.575462 +99444531.713,99.444532,-29.257259,-29.570935 +99444824.681,99.444825,-29.084055,-29.563939 +99445117.650,99.445118,-29.688122,-29.553059 +99445410.618,99.445411,-29.889360,-29.540545 +99445703.587,99.445704,-29.226955,-29.532432 +99445996.555,99.445997,-29.185869,-29.525371 +99446289.524,99.446290,-30.076813,-29.520652 +99446582.492,99.446582,-29.635389,-29.516156 +99446875.461,99.446875,-29.254519,-29.510797 +99447168.429,99.447168,-29.781326,-29.502320 +99447461.398,99.447461,-29.896769,-29.503782 +99447754.366,99.447754,-29.944651,-29.515522 +99448047.335,99.448047,-29.543289,-29.520240 +99448340.303,99.448340,-29.515139,-29.519563 +99448633.272,99.448633,-29.422092,-29.522003 +99448926.240,99.448926,-29.378723,-29.515660 +99449219.209,99.449219,-29.420847,-29.506420 +99449512.177,99.449512,-30.265593,-29.504800 +99449805.146,99.449805,-31.030445,-29.503343 +99450098.115,99.450098,-30.299835,-29.498165 +99450391.083,99.450391,-29.673466,-29.496086 +99450684.052,99.450684,-29.292356,-29.493806 +99450977.020,99.450977,-29.323961,-29.491704 +99451269.989,99.451270,-29.483072,-29.490032 +99451562.957,99.451563,-29.929752,-29.491399 +99451855.926,99.451856,-29.881929,-29.494091 +99452148.894,99.452149,-29.580395,-29.496334 +99452441.863,99.452442,-29.365799,-29.493142 +99452734.831,99.452735,-29.290077,-29.491105 +99453027.800,99.453028,-29.643106,-29.488611 +99453320.768,99.453321,-29.247007,-29.486045 +99453613.737,99.453614,-28.913816,-29.480400 +99453906.705,99.453907,-29.510851,-29.473331 +99454199.674,99.454200,-29.582987,-29.465288 +99454492.642,99.454493,-29.537518,-29.460133 +99454785.611,99.454786,-29.498501,-29.457243 +99455078.579,99.455079,-29.363651,-29.452393 +99455371.548,99.455372,-29.310358,-29.448084 +99455664.516,99.455665,-29.595684,-29.442999 +99455957.485,99.455957,-29.511671,-29.435646 +99456250.453,99.456250,-29.228615,-29.430594 +99456543.422,99.456543,-29.160898,-29.425442 +99456836.390,99.456836,-29.612665,-29.421618 +99457129.359,99.457129,-29.787779,-29.419745 +99457422.327,99.457422,-30.031940,-29.419856 +99457715.296,99.457715,-29.625942,-29.414087 +99458008.264,99.458008,-29.863966,-29.404674 +99458301.233,99.458301,-29.712154,-29.394810 +99458594.201,99.458594,-29.363422,-29.390782 +99458887.170,99.458887,-28.916430,-29.392180 +99459180.138,99.459180,-29.270126,-29.388617 +99459473.107,99.459473,-29.375772,-29.381641 +99459766.075,99.459766,-29.696775,-29.373433 +99460059.044,99.460059,-29.752140,-29.367593 +99460352.012,99.460352,-29.571213,-29.362625 +99460644.981,99.460645,-29.431936,-29.354919 +99460937.949,99.460938,-29.474270,-29.350728 +99461230.918,99.461231,-29.250454,-29.346137 +99461523.886,99.461524,-29.426985,-29.338870 +99461816.855,99.461817,-29.585939,-29.332170 +99462109.823,99.462110,-29.383261,-29.324110 +99462402.792,99.462403,-29.360275,-29.315582 +99462695.760,99.462696,-29.448839,-29.310336 +99462988.729,99.462989,-29.594122,-29.306242 +99463281.697,99.463282,-28.930357,-29.303191 +99463574.666,99.463575,-29.289410,-29.300929 +99463867.634,99.463868,-29.381641,-29.295498 +99464160.603,99.464161,-29.618464,-29.290997 +99464453.571,99.464454,-29.220751,-29.288556 +99464746.540,99.464747,-28.971540,-29.283701 +99465039.508,99.465040,-28.404722,-29.283063 +99465332.477,99.465332,-28.628788,-29.277010 +99465625.445,99.465625,-29.019014,-29.273140 +99465918.414,99.465918,-28.810366,-29.273011 +99466211.382,99.466211,-29.087690,-29.268573 +99466504.351,99.466504,-29.283298,-29.262573 +99466797.319,99.466797,-29.262182,-29.262847 +99467090.288,99.467090,-29.007675,-29.260302 +99467383.256,99.467383,-29.499861,-29.249819 +99467676.225,99.467676,-29.877115,-29.241707 +99467969.193,99.467969,-29.240627,-29.237697 +99468262.162,99.468262,-28.930645,-29.236808 +99468555.130,99.468555,-29.233973,-29.232487 +99468848.099,99.468848,-29.038124,-29.231446 +99469141.067,99.469141,-28.503094,-29.229617 +99469434.036,99.469434,-29.489229,-29.225564 +99469727.004,99.469727,-29.702076,-29.213488 +99470019.973,99.470020,-29.482424,-29.196825 +99470312.941,99.470313,-29.641888,-29.181597 +99470605.910,99.470606,-29.516413,-29.168423 +99470898.878,99.470899,-29.534410,-29.160036 +99471191.847,99.471192,-29.567625,-29.152167 +99471484.815,99.471485,-29.781942,-29.148063 +99471777.784,99.471778,-29.650446,-29.140736 +99472070.752,99.472071,-29.564142,-29.131505 +99472363.721,99.472364,-29.068930,-29.123705 +99472656.689,99.472657,-29.170279,-29.116626 +99472949.658,99.472950,-29.537964,-29.114364 +99473242.626,99.473243,-29.201500,-29.109917 +99473535.595,99.473536,-28.875845,-29.105070 +99473828.563,99.473829,-29.097893,-29.102093 +99474121.532,99.474122,-28.746101,-29.093363 +99474414.500,99.474415,-29.106127,-29.085052 +99474707.469,99.474707,-29.329819,-29.077778 +99475000.438,99.475000,-29.121811,-29.070865 +99475293.406,99.475293,-29.021751,-29.066496 +99475586.375,99.475586,-28.917112,-29.061130 +99475879.343,99.475879,-28.917978,-29.052456 +99476172.312,99.476172,-28.969542,-29.049023 +99476465.280,99.476465,-29.073729,-29.046554 +99476758.249,99.476758,-29.278852,-29.041351 +99477051.217,99.477051,-29.602816,-29.039758 +99477344.186,99.477344,-29.645063,-29.038910 +99477637.154,99.477637,-28.777380,-29.029534 +99477930.123,99.477930,-28.557873,-29.022213 +99478223.091,99.478223,-28.694458,-29.012415 +99478516.060,99.478516,-28.889687,-29.003438 +99478809.028,99.478809,-29.115385,-28.994273 +99479101.997,99.479102,-28.847078,-28.991652 +99479394.965,99.479395,-28.632643,-28.984881 +99479687.934,99.479688,-28.565046,-28.973933 +99479980.902,99.479981,-29.048519,-28.957767 +99480273.871,99.480274,-28.714979,-28.941158 +99480566.839,99.480567,-28.726000,-28.929469 +99480859.808,99.480860,-29.069702,-28.919063 +99481152.776,99.481153,-28.999397,-28.906125 +99481445.745,99.481446,-28.725851,-28.899721 +99481738.713,99.481739,-28.725019,-28.892367 +99482031.682,99.482032,-28.487968,-28.879208 +99482324.650,99.482325,-28.336624,-28.864765 +99482617.619,99.482618,-28.909422,-28.846957 +99482910.587,99.482911,-29.166067,-28.825250 +99483203.556,99.483204,-29.122583,-28.805335 +99483496.524,99.483497,-29.079573,-28.791576 +99483789.493,99.483789,-28.976641,-28.771002 +99484082.461,99.484082,-28.827414,-28.748109 +99484375.430,99.484375,-29.083357,-28.731504 +99484668.398,99.484668,-28.642225,-28.717984 +99484961.367,99.484961,-29.001299,-28.701351 +99485254.335,99.485254,-28.861153,-28.682936 +99485547.304,99.485547,-29.317913,-28.669372 +99485840.272,99.485840,-29.209471,-28.649267 +99486133.241,99.486133,-28.627224,-28.624148 +99486426.209,99.486426,-29.184296,-28.599850 +99486719.178,99.486719,-29.676493,-28.570711 +99487012.146,99.487012,-28.919701,-28.543380 +99487305.115,99.487305,-28.403673,-28.519690 +99487598.083,99.487598,-28.766378,-28.488776 +99487891.052,99.487891,-29.342312,-28.459510 +99488184.020,99.488184,-29.414474,-28.433314 +99488476.989,99.488477,-28.925547,-28.399013 +99488769.957,99.488770,-29.275587,-28.364407 +99489062.926,99.489063,-29.126953,-28.333447 +99489355.894,99.489356,-28.876020,-28.305782 +99489648.863,99.489649,-28.549236,-28.264099 +99489941.831,99.489942,-28.477156,-28.222838 +99490234.800,99.490235,-28.203707,-28.183400 +99490527.768,99.490528,-28.011793,-28.143145 +99490820.737,99.490821,-28.245752,-28.095392 +99491113.705,99.491114,-28.327055,-28.049634 +99491406.674,99.491407,-28.914871,-27.992805 +99491699.642,99.491700,-28.868071,-27.933113 +99491992.611,99.491993,-28.592331,-27.867626 +99492285.579,99.492286,-28.531689,-27.799118 +99492578.548,99.492579,-28.444614,-27.736188 +99492871.516,99.492872,-28.978931,-27.657128 +99493164.485,99.493164,-29.004045,-27.572971 +99493457.453,99.493457,-28.610319,-27.473062 +99493750.422,99.493750,-28.539968,-27.376667 +99494043.390,99.494043,-28.361809,-27.297814 +99494336.359,99.494336,-28.464539,-27.217006 +99494629.327,99.494629,-28.551411,-27.115688 +99494922.296,99.494922,-28.562761,-27.018722 +99495215.264,99.495215,-28.766138,-26.933372 +99495508.233,99.495508,-28.594622,-26.845793 +99495801.201,99.495801,-28.423727,-26.749494 +99496094.170,99.496094,-29.017841,-26.648068 +99496387.138,99.496387,-28.889532,-26.553358 +99496680.107,99.496680,-28.484251,-26.469820 +99496973.075,99.496973,-29.370466,-26.383699 +99497266.044,99.497266,-29.651937,-26.294850 +99497559.012,99.497559,-28.657953,-26.205268 +99497851.981,99.497852,-28.604450,-26.126549 +99498144.949,99.498145,-28.479044,-26.052184 +99498437.918,99.498438,-28.463697,-25.978741 +99498730.886,99.498731,-28.176023,-25.911369 +99499023.855,99.499024,-28.577271,-25.843971 +99499316.823,99.499317,-28.378372,-25.771442 +99499609.792,99.499610,-27.982182,-25.694464 +99499902.760,99.499903,-27.646238,-25.623032 +99500195.729,99.500196,-27.629450,-25.546827 +99500488.698,99.500489,-28.027412,-25.473104 +99500781.666,99.500782,-28.068003,-25.398194 +99501074.635,99.501075,-27.817408,-25.321477 +99501367.603,99.501368,-28.390945,-25.234026 +99501660.572,99.501661,-28.415926,-25.150988 +99501953.540,99.501954,-27.860659,-25.057115 +99502246.509,99.502247,-27.586248,-24.968149 +99502539.477,99.502539,-27.235849,-24.897260 +99502832.446,99.502832,-26.923948,-24.822151 +99503125.414,99.503125,-27.159645,-24.742467 +99503418.383,99.503418,-27.324427,-24.677151 +99503711.351,99.503711,-26.910040,-24.604052 +99504004.320,99.504004,-26.749125,-24.530022 +99504297.288,99.504297,-27.466652,-24.444126 +99504590.257,99.504590,-27.523693,-24.360411 +99504883.225,99.504883,-27.047850,-24.276005 +99505176.194,99.505176,-26.519011,-24.190966 +99505469.162,99.505469,-27.096018,-24.082890 +99505762.131,99.505762,-26.742794,-23.961699 +99506055.099,99.506055,-26.198360,-23.856045 +99506348.068,99.506348,-26.403795,-23.750333 +99506641.036,99.506641,-26.103735,-23.626406 +99506934.005,99.506934,-26.221239,-23.484939 +99507226.973,99.507227,-26.373957,-23.365782 +99507519.942,99.507520,-26.023695,-23.245998 +99507812.910,99.507813,-26.306810,-23.100454 +99508105.879,99.508106,-26.247259,-22.941883 +99508398.847,99.508399,-25.470821,-22.772265 +99508691.816,99.508692,-25.563435,-22.586061 +99508984.784,99.508985,-25.724653,-22.388010 +99509277.753,99.509278,-25.691914,-22.153773 +99509570.721,99.509571,-25.119507,-21.875834 +99509863.690,99.509864,-25.197908,-21.594805 +99510156.658,99.510157,-25.216875,-21.322356 +99510449.627,99.510450,-25.193739,-21.047009 +99510742.595,99.510743,-24.633650,-20.731238 +99511035.564,99.511036,-24.732723,-20.427383 +99511328.532,99.511329,-24.053793,-20.158727 +99511621.501,99.511622,-23.913210,-19.883925 +99511914.469,99.511914,-23.532087,-19.547304 +99512207.438,99.512207,-23.311760,-19.102721 +99512500.406,99.512500,-23.411993,-18.601814 +99512793.375,99.512793,-22.636656,-18.185394 +99513086.343,99.513086,-22.433762,-17.807702 +99513379.312,99.513379,-21.689705,-17.411367 +99513672.280,99.513672,-21.676601,-16.972943 +99513965.249,99.513965,-22.348370,-16.614912 +99514258.217,99.514258,-22.126621,-16.347215 +99514551.186,99.514551,-21.327608,-16.069717 +99514844.154,99.514844,-21.440920,-15.787527 +99515137.123,99.515137,-21.787481,-15.536058 +99515430.091,99.515430,-21.607103,-15.279844 +99515723.060,99.515723,-21.172693,-15.038525 +99516016.028,99.516016,-20.901541,-14.787444 +99516308.997,99.516309,-21.077299,-14.559609 +99516601.965,99.516602,-21.474165,-14.364045 +99516894.934,99.516895,-21.327597,-14.194647 +99517187.902,99.517188,-21.187830,-14.041096 +99517480.871,99.517481,-21.086597,-13.899282 +99517773.839,99.517774,-21.349562,-13.777422 +99518066.808,99.518067,-21.446913,-13.650959 +99518359.776,99.518360,-21.457209,-13.521719 +99518652.745,99.518653,-21.740671,-13.386111 +99518945.713,99.518946,-21.726919,-13.268829 +99519238.682,99.519239,-21.360025,-13.155382 +99519531.650,99.519532,-21.047632,-13.033745 +99519824.619,99.519825,-21.241045,-12.914504 +99520117.587,99.520118,-21.029787,-12.805335 +99520410.556,99.520411,-21.032593,-12.685702 +99520703.524,99.520704,-20.914946,-12.571926 +99520996.493,99.520996,-20.817909,-12.453647 +99521289.461,99.521289,-20.249111,-12.322161 +99521582.430,99.521582,-20.331358,-12.205788 +99521875.398,99.521875,-19.794716,-12.096701 +99522168.367,99.522168,-19.886141,-11.996359 +99522461.335,99.522461,-20.630461,-11.908672 +99522754.304,99.522754,-20.445967,-11.827446 +99523047.272,99.523047,-20.191206,-11.753366 +99523340.241,99.523340,-20.867262,-11.667823 +99523633.209,99.523633,-20.382341,-11.568700 +99523926.178,99.523926,-20.257214,-11.455094 +99524219.146,99.524219,-19.604708,-11.332075 +99524512.115,99.524512,-19.658384,-11.237597 +99524805.083,99.524805,-19.500717,-11.162338 +99525098.052,99.525098,-19.438839,-11.089893 +99525391.021,99.525391,-18.400837,-11.020373 +99525683.989,99.525684,-17.874826,-10.940731 +99525976.958,99.525977,-18.312143,-10.853761 +99526269.926,99.526270,-18.163481,-10.775668 +99526562.895,99.526563,-17.464603,-10.704341 +99526855.863,99.526856,-16.828127,-10.634837 +99527148.832,99.527149,-17.360268,-10.563058 +99527441.800,99.527442,-17.190832,-10.483557 +99527734.769,99.527735,-16.300352,-10.399217 +99528027.737,99.528028,-15.831363,-10.306869 +99528320.706,99.528321,-15.396999,-10.228883 +99528613.674,99.528614,-14.817582,-10.155116 +99528906.643,99.528907,-14.386293,-10.092344 +99529199.611,99.529200,-13.464907,-10.023279 +99529492.580,99.529493,-12.484364,-9.943293 +99529785.548,99.529786,-12.156862,-9.867567 +99530078.517,99.530079,-12.016407,-9.800557 +99530371.485,99.530371,-11.697598,-9.743002 +99530664.454,99.530664,-10.821355,-9.685480 +99530957.422,99.530957,-10.685550,-9.624831 +99531250.391,99.531250,-10.931203,-9.556751 +99531543.359,99.531543,-10.576896,-9.483057 +99531836.328,99.531836,-9.404335,-9.413293 +99532129.296,99.532129,-7.817739,-9.347205 +99532422.265,99.532422,-6.833453,-9.279029 +99532715.233,99.532715,-7.174717,-9.219597 +99533008.202,99.533008,-7.205481,-9.158757 +99533301.170,99.533301,-6.612927,-9.096836 +99533594.139,99.533594,-5.759415,-9.043796 +99533887.107,99.533887,-6.238495,-8.991286 +99534180.076,99.534180,-7.181351,-8.944703 +99534473.044,99.534473,-6.756356,-8.896552 +99534766.013,99.534766,-6.406501,-8.846876 +99535058.981,99.535059,-6.639003,-8.804359 +99535351.950,99.535352,-6.307206,-8.758325 +99535644.918,99.535645,-6.317564,-8.713523 +99535937.887,99.535938,-5.900518,-8.676515 +99536230.855,99.536231,-6.085300,-8.641600 +99536523.824,99.536524,-6.533882,-8.602735 +99536816.792,99.536817,-6.969864,-8.560919 +99537109.761,99.537110,-7.239102,-8.519074 +99537402.729,99.537403,-7.437279,-8.483148 +99537695.698,99.537696,-7.953147,-8.445098 +99537988.666,99.537989,-7.669890,-8.404569 +99538281.635,99.538282,-7.448434,-8.364231 +99538574.603,99.538575,-7.109632,-8.326230 +99538867.572,99.538868,-7.607398,-8.293539 +99539160.540,99.539161,-7.639525,-8.261597 +99539453.509,99.539454,-7.221032,-8.231114 +99539746.477,99.539746,-7.184047,-8.197584 +99540039.446,99.540039,-7.447768,-8.164827 +99540332.414,99.540332,-6.940318,-8.132584 +99540625.383,99.540625,-7.044024,-8.099772 +99540918.351,99.540918,-6.761949,-8.067277 +99541211.320,99.541211,-6.179706,-8.036620 +99541504.288,99.541504,-6.587202,-8.005339 +99541797.257,99.541797,-6.754283,-7.974214 +99542090.225,99.542090,-7.006452,-7.939359 +99542383.194,99.542383,-7.491035,-7.905540 +99542676.162,99.542676,-7.732691,-7.871947 +99542969.131,99.542969,-8.047341,-7.841296 +99543262.099,99.543262,-7.353703,-7.811513 +99543555.068,99.543555,-6.629893,-7.779371 +99543848.036,99.543848,-5.933548,-7.747756 +99544141.005,99.544141,-5.471863,-7.715631 +99544433.973,99.544434,-6.507115,-7.682788 +99544726.942,99.544727,-7.402542,-7.651533 +99545019.910,99.545020,-7.488314,-7.624731 +99545312.879,99.545313,-7.588706,-7.593613 +99545605.847,99.545606,-6.937635,-7.559409 +99545898.816,99.545899,-6.473072,-7.528794 +99546191.784,99.546192,-6.848430,-7.499490 +99546484.753,99.546485,-7.165987,-7.470837 +99546777.721,99.546778,-7.203769,-7.443171 +99547070.690,99.547071,-6.997362,-7.417058 +99547363.658,99.547364,-6.485360,-7.390226 +99547656.627,99.547657,-6.146714,-7.360493 +99547949.595,99.547950,-5.671653,-7.328411 +99548242.564,99.548243,-6.313958,-7.296991 +99548535.532,99.548536,-6.468930,-7.263021 +99548828.501,99.548829,-7.094474,-7.229658 +99549121.469,99.549121,-6.622101,-7.201392 +99549414.438,99.549414,-5.917699,-7.177059 +99549707.406,99.549707,-6.069215,-7.151602 +99550000.375,99.550000,-6.523868,-7.121751 +99550293.344,99.550293,-7.113851,-7.089389 +99550586.312,99.550586,-7.059241,-7.058953 +99550879.281,99.550879,-6.765966,-7.038366 +99551172.249,99.551172,-6.209733,-7.022535 +99551465.218,99.551465,-5.790887,-7.003640 +99551758.186,99.551758,-5.952430,-6.986551 +99552051.155,99.552051,-6.110297,-6.976206 +99552344.123,99.552344,-5.907733,-6.972629 +99552637.092,99.552637,-6.431715,-6.974258 +99552930.060,99.552930,-6.256783,-6.971879 +99553223.029,99.553223,-6.117379,-6.965348 +99553515.997,99.553516,-6.695168,-6.965032 +99553808.966,99.553809,-6.685896,-6.975652 +99554101.934,99.554102,-7.162722,-6.983548 +99554394.903,99.554395,-6.970591,-6.984578 +99554687.871,99.554688,-6.766280,-6.987522 +99554980.840,99.554981,-7.376737,-6.991231 +99555273.808,99.555274,-7.014057,-6.992759 +99555566.777,99.555567,-7.077707,-6.998099 +99555859.745,99.555860,-7.817366,-7.000725 +99556152.714,99.556153,-8.010505,-7.008124 +99556445.682,99.556446,-7.540427,-7.013269 +99556738.651,99.556739,-7.213098,-7.013106 +99557031.619,99.557032,-7.164413,-7.009055 +99557324.588,99.557325,-7.755967,-7.005093 +99557617.556,99.557618,-7.477690,-6.999948 +99557910.525,99.557911,-7.188008,-6.988420 +99558203.493,99.558203,-7.172418,-6.976365 +99558496.462,99.558496,-7.384496,-6.968043 +99558789.430,99.558789,-7.989135,-6.964066 +99559082.399,99.559082,-8.054039,-6.957387 +99559375.367,99.559375,-8.201254,-6.948296 +99559668.336,99.559668,-7.761929,-6.942183 +99559961.304,99.559961,-7.835862,-6.937618 +99560254.273,99.560254,-7.860506,-6.931435 +99560547.241,99.560547,-7.757132,-6.930426 +99560840.210,99.560840,-7.760701,-6.930661 +99561133.178,99.561133,-7.965984,-6.931196 +99561426.147,99.561426,-7.823042,-6.935113 +99561719.115,99.561719,-7.818756,-6.934747 +99562012.084,99.562012,-7.293427,-6.932812 +99562305.052,99.562305,-7.389933,-6.929576 +99562598.021,99.562598,-7.424113,-6.921897 +99562890.989,99.562891,-7.762229,-6.913052 +99563183.958,99.563184,-7.837121,-6.900223 +99563476.926,99.563477,-7.529150,-6.894217 +99563769.895,99.563770,-7.543240,-6.891350 +99564062.863,99.564063,-7.440676,-6.894555 +99564355.832,99.564356,-7.283270,-6.903211 +99564648.800,99.564649,-7.458687,-6.901568 +99564941.769,99.564942,-8.044827,-6.894047 +99565234.737,99.565235,-7.408560,-6.885262 +99565527.706,99.565528,-6.925694,-6.872101 +99565820.674,99.565821,-7.297355,-6.865777 +99566113.643,99.566114,-7.481230,-6.860164 +99566406.611,99.566407,-7.531429,-6.852041 +99566699.580,99.566700,-7.577797,-6.843304 +99566992.548,99.566993,-7.705161,-6.833054 +99567285.517,99.567286,-7.638208,-6.820803 +99567578.485,99.567578,-7.195520,-6.816209 +99567871.454,99.567871,-6.778383,-6.817234 +99568164.422,99.568164,-6.775008,-6.817112 +99568457.391,99.568457,-6.395521,-6.806982 +99568750.359,99.568750,-6.356560,-6.797049 +99569043.328,99.569043,-6.867168,-6.787188 +99569336.296,99.569336,-7.174765,-6.782323 +99569629.265,99.569629,-6.763038,-6.782704 +99569922.233,99.569922,-6.145270,-6.780942 +99570215.202,99.570215,-5.822733,-6.770712 +99570508.170,99.570508,-5.918086,-6.753578 +99570801.139,99.570801,-6.759181,-6.733659 +99571094.107,99.571094,-7.343605,-6.714182 +99571387.076,99.571387,-7.005595,-6.701757 +99571680.044,99.571680,-7.094337,-6.696996 +99571973.013,99.571973,-7.444966,-6.694322 +99572265.981,99.572266,-7.260011,-6.694267 +99572558.950,99.572559,-7.055194,-6.697604 +99572851.918,99.572852,-6.846050,-6.695285 +99573144.887,99.573145,-6.350543,-6.693849 +99573437.855,99.573438,-6.573177,-6.692620 +99573730.824,99.573731,-7.029006,-6.685790 +99574023.792,99.574024,-7.262348,-6.678393 +99574316.761,99.574317,-7.331479,-6.667587 +99574609.729,99.574610,-7.156833,-6.657944 +99574902.698,99.574903,-6.875228,-6.648774 +99575195.667,99.575196,-6.836464,-6.634279 +99575488.635,99.575489,-6.981213,-6.627969 +99575781.604,99.575782,-6.636516,-6.621773 +99576074.572,99.576075,-6.767813,-6.610484 +99576367.541,99.576368,-6.695788,-6.593763 +99576660.509,99.576661,-6.513917,-6.582162 +99576953.478,99.576953,-6.452345,-6.571218 +99577246.446,99.577246,-6.701226,-6.559691 +99577539.415,99.577539,-6.720676,-6.546237 +99577832.383,99.577832,-6.328971,-6.536427 +99578125.352,99.578125,-6.069656,-6.528876 +99578418.320,99.578418,-6.332693,-6.522280 +99578711.289,99.578711,-6.580068,-6.511975 +99579004.257,99.579004,-6.657321,-6.497292 +99579297.226,99.579297,-6.381784,-6.479897 +99579590.194,99.579590,-6.409521,-6.459907 +99579883.163,99.579883,-6.568211,-6.446952 +99580176.131,99.580176,-6.585944,-6.439093 +99580469.100,99.580469,-6.803881,-6.428902 +99580762.068,99.580762,-7.077210,-6.417457 +99581055.037,99.581055,-6.833103,-6.405229 +99581348.005,99.581348,-6.656013,-6.390542 +99581640.974,99.581641,-6.541147,-6.379717 +99581933.942,99.581934,-6.507150,-6.367894 +99582226.911,99.582227,-6.577206,-6.360102 +99582519.879,99.582520,-6.432887,-6.352956 +99582812.848,99.582813,-6.472543,-6.342254 +99583105.816,99.583106,-6.211003,-6.326399 +99583398.785,99.583399,-6.524271,-6.311479 +99583691.753,99.583692,-6.275084,-6.297137 +99583984.722,99.583985,-6.300621,-6.283868 +99584277.690,99.584278,-6.422527,-6.273505 +99584570.659,99.584571,-6.306321,-6.263865 +99584863.627,99.584864,-6.376526,-6.252572 +99585156.596,99.585157,-6.288828,-6.240257 +99585449.564,99.585450,-5.860055,-6.232692 +99585742.533,99.585743,-6.136210,-6.226293 +99586035.501,99.586036,-5.821517,-6.219580 +99586328.470,99.586328,-5.859205,-6.210872 +99586621.438,99.586621,-6.038012,-6.199342 +99586914.407,99.586914,-5.894883,-6.185832 +99587207.375,99.587207,-5.530075,-6.171697 +99587500.344,99.587500,-5.938358,-6.161423 +99587793.312,99.587793,-6.268762,-6.153859 +99588086.281,99.588086,-5.658892,-6.146770 +99588379.249,99.588379,-5.225101,-6.138388 +99588672.218,99.588672,-5.362380,-6.130579 +99588965.186,99.588965,-5.844564,-6.125457 +99589258.155,99.589258,-6.023319,-6.119565 +99589551.123,99.589551,-5.960728,-6.111985 +99589844.092,99.589844,-5.869049,-6.107326 +99590137.060,99.590137,-5.369283,-6.107651 +99590430.029,99.590430,-5.110632,-6.108499 +99590722.997,99.590723,-4.817180,-6.104312 +99591015.966,99.591016,-4.669782,-6.095888 +99591308.934,99.591309,-4.908132,-6.082971 +99591601.903,99.591602,-5.291561,-6.069866 +99591894.871,99.591895,-5.654222,-6.057831 +99592187.840,99.592188,-6.103680,-6.046594 +99592480.808,99.592481,-6.306813,-6.036305 +99592773.777,99.592774,-6.142599,-6.026814 +99593066.745,99.593067,-6.082450,-6.019361 +99593359.714,99.593360,-5.972358,-6.017488 +99593652.682,99.593653,-5.846770,-6.012360 +99593945.651,99.593946,-5.774309,-6.001304 +99594238.619,99.594239,-5.759051,-5.992856 +99594531.588,99.594532,-5.743385,-5.985753 +99594824.556,99.594825,-5.637253,-5.978478 +99595117.525,99.595118,-5.497066,-5.974260 +99595410.493,99.595410,-6.159611,-5.971604 +99595703.462,99.595703,-6.225142,-5.968860 +99595996.430,99.595996,-6.145311,-5.968784 +99596289.399,99.596289,-5.631255,-5.967862 +99596582.367,99.596582,-5.909748,-5.964238 +99596875.336,99.596875,-5.752723,-5.961158 +99597168.304,99.597168,-5.649426,-5.957626 +99597461.273,99.597461,-5.825190,-5.952134 +99597754.241,99.597754,-6.066477,-5.945185 +99598047.210,99.598047,-6.126108,-5.936932 +99598340.178,99.598340,-6.233048,-5.929406 +99598633.147,99.598633,-5.932696,-5.921687 +99598926.115,99.598926,-5.809237,-5.911383 +99599219.084,99.599219,-5.533089,-5.906288 +99599512.052,99.599512,-5.324444,-5.901899 +99599805.021,99.599805,-5.851927,-5.900064 +99600097.990,99.600098,-6.559500,-5.890028 +99600390.958,99.600391,-6.257381,-5.886464 +99600683.927,99.600684,-6.022770,-5.880134 +99600976.895,99.600977,-5.924393,-5.871467 +99601269.864,99.601270,-5.752316,-5.864050 +99601562.832,99.601563,-6.133701,-5.859721 +99601855.801,99.601856,-5.999772,-5.859257 +99602148.769,99.602149,-6.139151,-5.858503 +99602441.738,99.602442,-6.297572,-5.855189 +99602734.706,99.602735,-5.862952,-5.853658 +99603027.675,99.603028,-5.470906,-5.852711 +99603320.643,99.603321,-5.616138,-5.854192 +99603613.612,99.603614,-5.488401,-5.852333 +99603906.580,99.603907,-5.616110,-5.854264 +99604199.549,99.604200,-5.895802,-5.857614 +99604492.517,99.604493,-5.871591,-5.863393 +99604785.486,99.604785,-5.786436,-5.870341 +99605078.454,99.605078,-6.036937,-5.875690 +99605371.423,99.605371,-6.227381,-5.881010 +99605664.391,99.605664,-6.002081,-5.888723 +99605957.360,99.605957,-6.255200,-5.894843 +99606250.328,99.606250,-6.121462,-5.901189 +99606543.297,99.606543,-5.789015,-5.906224 +99606836.265,99.606836,-5.574025,-5.913083 +99607129.234,99.607129,-5.573391,-5.922024 +99607422.202,99.607422,-6.010203,-5.935810 +99607715.171,99.607715,-6.045986,-5.943006 +99608008.139,99.608008,-5.780086,-5.948150 +99608301.108,99.608301,-5.616041,-5.960478 +99608594.076,99.608594,-5.386353,-5.972847 +99608887.045,99.608887,-5.673506,-5.987027 +99609180.013,99.609180,-6.001294,-5.997481 +99609472.982,99.609473,-6.018292,-6.002682 +99609765.950,99.609766,-6.077973,-6.008260 +99610058.919,99.610059,-6.190455,-6.016859 +99610351.887,99.610352,-5.932981,-6.033287 +99610644.856,99.610645,-5.400865,-6.053395 +99610937.824,99.610938,-5.588821,-6.075599 +99611230.793,99.611231,-5.457356,-6.098947 +99611523.761,99.611524,-5.210059,-6.114763 +99611816.730,99.611817,-5.387800,-6.126552 +99612109.698,99.612110,-5.717544,-6.137051 +99612402.667,99.612403,-5.707576,-6.145203 +99612695.635,99.612696,-5.659655,-6.155866 +99612988.604,99.612989,-5.759761,-6.166020 +99613281.572,99.613282,-6.082024,-6.175515 +99613574.541,99.613575,-5.839815,-6.186337 +99613867.509,99.613868,-5.441448,-6.194762 +99614160.478,99.614160,-5.937700,-6.205112 +99614453.446,99.614453,-6.174674,-6.209803 +99614746.415,99.614746,-6.014922,-6.214548 +99615039.383,99.615039,-6.216878,-6.219092 +99615332.352,99.615332,-6.413830,-6.227810 +99615625.320,99.615625,-6.531016,-6.233246 +99615918.289,99.615918,-6.624310,-6.240965 +99616211.257,99.616211,-6.618616,-6.242936 +99616504.226,99.616504,-6.144916,-6.243679 +99616797.194,99.616797,-6.059470,-6.250483 +99617090.163,99.617090,-5.941527,-6.261082 +99617383.131,99.617383,-5.888767,-6.267488 +99617676.100,99.617676,-5.710571,-6.274519 +99617969.068,99.617969,-5.241043,-6.281583 +99618262.037,99.618262,-5.118023,-6.285257 +99618555.005,99.618555,-5.303428,-6.290004 +99618847.974,99.618848,-5.185373,-6.302053 +99619140.942,99.619141,-5.898391,-6.312925 +99619433.911,99.619434,-5.757608,-6.323052 +99619726.879,99.619727,-6.135749,-6.335505 +99620019.848,99.620020,-5.201985,-6.344190 +99620312.816,99.620313,-6.047480,-6.347552 +99620605.785,99.620606,-5.845777,-6.353817 +99620898.753,99.620899,-5.740112,-6.363873 +99621191.722,99.621192,-5.719771,-6.372741 +99621484.690,99.621485,-5.996593,-6.381549 +99621777.659,99.621778,-6.467453,-6.388626 +99622070.627,99.622071,-6.388784,-6.399414 +99622363.596,99.622364,-6.071936,-6.409093 +99622656.564,99.622657,-6.199643,-6.418354 +99622949.533,99.622950,-6.325318,-6.432248 +99623242.501,99.623243,-6.437170,-6.448130 +99623535.470,99.623535,-6.236761,-6.461929 +99623828.438,99.623828,-6.576935,-6.475177 +99624121.407,99.624121,-6.841354,-6.487562 +99624414.375,99.624414,-7.432287,-6.497466 +99624707.344,99.624707,-7.512710,-6.508302 +99625000.312,99.625000,-7.288789,-6.522202 +99625293.281,99.625293,-7.174374,-6.529121 +99625586.250,99.625586,-7.062910,-6.537331 +99625879.218,99.625879,-7.128517,-6.549853 +99626172.187,99.626172,-6.772544,-6.562178 +99626465.155,99.626465,-6.602142,-6.576625 +99626758.124,99.626758,-7.132900,-6.591148 +99627051.092,99.627051,-7.325060,-6.605323 +99627344.061,99.627344,-7.725049,-6.623321 +99627637.029,99.627637,-7.056215,-6.639218 +99627929.998,99.627930,-7.103221,-6.654101 +99628222.966,99.628223,-7.619568,-6.667945 +99628515.935,99.628516,-6.957625,-6.677714 +99628808.903,99.628809,-7.494977,-6.685738 +99629101.872,99.629102,-7.511497,-6.694911 +99629394.840,99.629395,-6.806108,-6.703833 +99629687.809,99.629688,-6.791736,-6.714892 +99629980.777,99.629981,-7.191121,-6.721067 +99630273.746,99.630274,-7.931148,-6.725687 +99630566.714,99.630567,-8.223836,-6.735818 +99630859.683,99.630860,-8.055608,-6.757565 +99631152.651,99.631153,-7.961933,-6.778284 +99631445.620,99.631446,-6.974234,-6.801872 +99631738.588,99.631739,-6.885856,-6.826573 +99632031.557,99.632032,-7.182888,-6.848194 +99632324.525,99.632325,-7.381897,-6.861924 +99632617.494,99.632617,-8.168934,-6.875560 +99632910.462,99.632910,-7.810771,-6.890213 +99633203.431,99.633203,-7.591323,-6.904595 +99633496.399,99.633496,-7.681803,-6.918142 +99633789.368,99.633789,-7.070322,-6.935160 +99634082.336,99.634082,-7.298762,-6.955364 +99634375.305,99.634375,-6.381138,-6.971642 +99634668.273,99.634668,-6.370068,-6.984884 +99634961.242,99.634961,-6.219384,-6.999961 +99635254.210,99.635254,-6.647145,-7.011737 +99635547.179,99.635547,-6.962065,-7.020413 +99635840.147,99.635840,-7.433101,-7.030128 +99636133.116,99.636133,-6.417818,-7.043341 +99636426.084,99.636426,-5.720627,-7.053269 +99636719.053,99.636719,-6.871156,-7.066001 +99637012.021,99.637012,-7.288019,-7.076285 +99637304.990,99.637305,-6.486959,-7.088557 +99637597.958,99.637598,-6.795176,-7.101885 +99637890.927,99.637891,-7.102567,-7.120008 +99638183.895,99.638184,-6.640886,-7.144376 +99638476.864,99.638477,-6.928034,-7.168345 +99638769.832,99.638770,-7.800724,-7.192564 +99639062.801,99.639063,-7.394349,-7.216354 +99639355.769,99.639356,-6.879899,-7.231407 +99639648.738,99.639649,-6.946574,-7.250605 +99639941.706,99.639942,-7.071721,-7.269813 +99640234.675,99.640235,-7.072460,-7.295332 +99640527.643,99.640528,-7.190874,-7.314078 +99640820.612,99.640821,-7.532366,-7.333141 +99641113.580,99.641114,-7.188667,-7.352869 +99641406.549,99.641407,-6.947631,-7.374255 +99641699.517,99.641700,-7.161314,-7.392590 +99641992.486,99.641992,-7.616112,-7.401903 +99642285.454,99.642285,-7.609710,-7.417384 +99642578.423,99.642578,-7.751197,-7.437334 +99642871.391,99.642871,-7.974655,-7.457839 +99643164.360,99.643164,-7.695061,-7.477278 +99643457.328,99.643457,-7.546446,-7.495943 +99643750.297,99.643750,-7.250268,-7.517155 +99644043.265,99.644043,-7.291806,-7.529786 +99644336.234,99.644336,-7.276327,-7.538535 +99644629.202,99.644629,-7.393217,-7.543982 +99644922.171,99.644922,-7.798133,-7.546482 +99645215.139,99.645215,-6.979195,-7.544883 +99645508.108,99.645508,-7.427094,-7.546746 +99645801.076,99.645801,-7.864395,-7.551168 +99646094.045,99.646094,-8.212310,-7.552505 +99646387.013,99.646387,-8.420803,-7.559225 +99646679.982,99.646680,-7.875602,-7.569790 +99646972.950,99.646973,-7.466513,-7.570367 +99647265.919,99.647266,-8.144250,-7.561281 +99647558.887,99.647559,-8.487008,-7.552625 +99647851.856,99.647852,-8.329034,-7.553472 +99648144.824,99.648145,-7.694719,-7.563370 +99648437.793,99.648438,-6.817132,-7.572831 +99648730.761,99.648731,-6.289909,-7.587741 +99649023.730,99.649024,-6.802764,-7.599572 +99649316.698,99.649317,-7.191306,-7.615576 +99649609.667,99.649610,-7.552027,-7.639943 +99649902.635,99.649903,-6.877314,-7.666745 +99650195.604,99.650196,-6.789747,-7.688128 +99650488.573,99.650489,-7.275752,-7.707254 +99650781.541,99.650782,-8.439195,-7.723429 +99651074.510,99.651075,-8.583509,-7.741871 +99651367.478,99.651367,-8.900793,-7.762352 +99651660.447,99.651660,-8.568921,-7.792222 +99651953.415,99.651953,-8.299229,-7.823919 +99652246.384,99.652246,-7.478306,-7.854432 +99652539.352,99.652539,-7.441955,-7.884962 +99652832.321,99.652832,-7.522313,-7.909248 +99653125.289,99.653125,-7.624765,-7.937546 +99653418.258,99.653418,-7.973473,-7.968305 +99653711.226,99.653711,-8.190331,-7.997441 +99654004.195,99.654004,-8.044314,-8.031581 +99654297.163,99.654297,-8.195847,-8.065051 +99654590.132,99.654590,-8.026648,-8.108767 +99654883.100,99.654883,-8.089280,-8.153170 +99655176.069,99.655176,-7.826965,-8.200052 +99655469.037,99.655469,-7.595675,-8.242337 +99655762.006,99.655762,-7.918679,-8.281510 +99656054.974,99.656055,-8.693786,-8.316721 +99656347.943,99.656348,-8.067496,-8.362167 +99656640.911,99.656641,-7.854164,-8.416813 +99656933.880,99.656934,-7.349516,-8.458485 +99657226.848,99.657227,-7.476275,-8.496778 +99657519.817,99.657520,-7.553876,-8.543970 +99657812.785,99.657813,-8.032530,-8.588640 +99658105.754,99.658106,-8.228110,-8.630806 +99658398.722,99.658399,-7.899028,-8.678167 +99658691.691,99.658692,-8.278166,-8.722011 +99658984.659,99.658985,-7.954500,-8.757422 +99659277.628,99.659278,-7.768862,-8.797721 +99659570.596,99.659571,-8.201826,-8.843883 +99659863.565,99.659864,-8.874108,-8.889637 +99660156.533,99.660157,-8.200093,-8.935156 +99660449.502,99.660450,-8.586767,-8.981570 +99660742.470,99.660742,-8.271043,-9.026679 +99661035.439,99.661035,-8.183293,-9.068344 +99661328.407,99.661328,-8.412330,-9.114211 +99661621.376,99.661621,-8.364347,-9.163801 +99661914.344,99.661914,-7.643167,-9.211405 +99662207.313,99.662207,-8.507559,-9.254541 +99662500.281,99.662500,-8.752953,-9.298311 +99662793.250,99.662793,-9.083900,-9.340953 +99663086.218,99.663086,-9.094033,-9.381191 +99663379.187,99.663379,-9.132720,-9.424736 +99663672.155,99.663672,-9.242602,-9.470595 +99663965.124,99.663965,-8.254566,-9.520394 +99664258.092,99.664258,-8.006425,-9.569983 +99664551.061,99.664551,-8.228836,-9.620406 +99664844.029,99.664844,-7.866724,-9.670276 +99665136.998,99.665137,-7.086994,-9.715931 +99665429.966,99.665430,-7.415317,-9.772664 +99665722.935,99.665723,-7.641395,-9.823998 +99666015.903,99.666016,-7.297948,-9.870801 +99666308.872,99.666309,-7.618555,-9.913474 +99666601.840,99.666602,-7.951306,-9.954238 +99666894.809,99.666895,-7.204994,-10.002228 +99667187.777,99.667188,-6.283060,-10.056303 +99667480.746,99.667481,-6.639954,-10.102682 +99667773.714,99.667774,-7.160937,-10.146132 +99668066.683,99.668067,-8.537030,-10.192164 +99668359.651,99.668360,-9.180988,-10.246664 +99668652.620,99.668653,-9.230841,-10.315283 +99668945.588,99.668946,-9.466167,-10.394580 +99669238.557,99.669239,-10.468160,-10.466020 +99669531.525,99.669532,-11.197139,-10.532196 +99669824.494,99.669824,-11.925233,-10.593770 +99670117.462,99.670117,-11.204014,-10.667365 +99670410.431,99.670410,-12.327907,-10.744004 +99670703.399,99.670703,-11.926025,-10.813335 +99670996.368,99.670996,-12.334997,-10.866697 +99671289.336,99.671289,-12.883945,-10.919153 +99671582.305,99.671582,-13.613638,-10.968209 +99671875.273,99.671875,-14.066019,-11.021697 +99672168.242,99.672168,-14.816603,-11.079046 +99672461.210,99.672461,-15.944353,-11.150725 +99672754.179,99.672754,-15.510979,-11.224285 +99673047.147,99.673047,-16.665579,-11.297591 +99673340.116,99.673340,-17.289467,-11.370416 +99673633.084,99.673633,-16.144884,-11.438785 +99673926.053,99.673926,-15.816408,-11.504913 +99674219.021,99.674219,-16.824053,-11.574447 +99674511.990,99.674512,-17.999815,-11.642622 +99674804.958,99.674805,-18.163055,-11.714593 +99675097.927,99.675098,-18.611849,-11.786708 +99675390.896,99.675391,-18.097071,-11.864793 +99675683.864,99.675684,-17.723410,-11.948886 +99675976.833,99.675977,-17.888424,-12.028377 +99676269.801,99.676270,-17.442255,-12.095631 +99676562.770,99.676563,-17.422884,-12.174969 +99676855.738,99.676856,-17.654118,-12.260113 +99677148.707,99.677149,-18.177818,-12.358118 +99677441.675,99.677442,-18.169611,-12.455564 +99677734.644,99.677735,-18.816471,-12.553388 +99678027.612,99.678028,-19.329981,-12.642863 +99678320.581,99.678321,-18.839352,-12.730017 +99678613.549,99.678614,-17.880692,-12.826022 +99678906.518,99.678907,-17.794149,-12.915910 +99679199.486,99.679199,-18.616341,-13.015115 +99679492.455,99.679492,-18.515266,-13.121347 +99679785.423,99.679785,-18.347324,-13.219652 +99680078.392,99.680078,-19.280937,-13.305360 +99680371.360,99.680371,-19.947113,-13.407933 +99680664.329,99.680664,-19.218136,-13.503688 +99680957.297,99.680957,-18.913568,-13.609217 +99681250.266,99.681250,-18.969759,-13.719762 +99681543.234,99.681543,-19.502733,-13.827431 +99681836.203,99.681836,-19.511168,-13.939012 +99682129.171,99.682129,-19.676113,-14.074530 +99682422.140,99.682422,-19.877752,-14.188677 +99682715.108,99.682715,-19.786242,-14.299454 +99683008.077,99.683008,-19.077711,-14.404600 +99683301.045,99.683301,-19.062672,-14.512094 +99683594.014,99.683594,-19.335403,-14.621300 +99683886.982,99.683887,-19.242085,-14.730428 +99684179.951,99.684180,-18.908096,-14.872000 +99684472.919,99.684473,-18.987288,-15.027302 +99684765.888,99.684766,-19.374399,-15.179867 +99685058.856,99.685059,-19.406794,-15.352027 +99685351.825,99.685352,-19.720978,-15.567964 +99685644.793,99.685645,-19.505846,-15.778075 +99685937.762,99.685938,-19.750452,-15.987516 +99686230.730,99.686231,-18.884233,-16.226297 +99686523.699,99.686524,-18.765131,-16.460284 +99686816.667,99.686817,-19.607634,-16.688624 +99687109.636,99.687110,-20.102598,-16.976895 +99687402.604,99.687403,-20.150099,-17.363006 +99687695.573,99.687696,-20.714201,-17.751603 +99687988.541,99.687989,-21.390373,-18.127958 +99688281.510,99.688282,-21.818815,-18.423683 +99688574.478,99.688574,-22.837271,-18.695190 +99688867.447,99.688867,-23.515608,-18.980878 +99689160.415,99.689160,-23.285910,-19.270021 +99689453.384,99.689453,-23.155891,-19.513547 +99689746.352,99.689746,-22.935543,-19.729817 +99690039.321,99.690039,-23.509865,-19.920659 +99690332.289,99.690332,-24.071245,-20.158766 +99690625.258,99.690625,-23.523470,-20.351064 +99690918.226,99.690918,-23.453342,-20.572526 +99691211.195,99.691211,-24.156473,-20.783712 +99691504.163,99.691504,-23.690775,-20.977802 +99691797.132,99.691797,-23.146223,-21.148752 +99692090.100,99.692090,-22.297476,-21.308038 +99692383.069,99.692383,-24.207495,-21.445271 +99692676.037,99.692676,-24.335815,-21.553022 +99692969.006,99.692969,-24.045538,-21.675969 +99693261.974,99.693262,-24.180311,-21.771284 +99693554.943,99.693555,-24.706593,-21.854658 +99693847.911,99.693848,-25.155519,-21.966583 +99694140.880,99.694141,-25.221354,-22.091311 +99694433.848,99.694434,-25.222219,-22.192062 +99694726.817,99.694727,-24.859352,-22.268751 +99695019.785,99.695020,-24.890350,-22.343680 +99695312.754,99.695313,-25.020182,-22.411437 +99695605.722,99.695606,-25.310719,-22.489820 +99695898.691,99.695899,-25.450205,-22.578210 +99696191.659,99.696192,-25.052826,-22.664300 +99696484.628,99.696485,-25.264242,-22.762771 +99696777.596,99.696778,-25.672369,-22.864325 +99697070.565,99.697071,-26.146900,-22.962039 +99697363.533,99.697364,-26.597605,-23.049624 +99697656.502,99.697657,-26.427120,-23.140037 +99697949.470,99.697949,-26.718897,-23.217895 +99698242.439,99.698242,-26.441965,-23.288306 +99698535.407,99.698535,-26.077223,-23.369397 +99698828.376,99.698828,-26.469896,-23.473221 +99699121.344,99.699121,-26.863121,-23.582812 +99699414.313,99.699414,-27.417322,-23.672344 +99699707.281,99.699707,-27.325336,-23.766699 +99700000.250,99.700000,-26.914415,-23.867468 +99700293.219,99.700293,-27.066204,-23.948509 +99700586.187,99.700586,-26.673399,-24.018709 +99700879.156,99.700879,-26.739716,-24.104533 +99701172.124,99.701172,-27.082092,-24.199627 +99701465.093,99.701465,-27.750992,-24.295354 +99701758.061,99.701758,-27.486399,-24.380055 +99702051.030,99.702051,-26.715977,-24.461533 +99702343.998,99.702344,-27.139338,-24.525902 +99702636.967,99.702637,-27.591366,-24.575837 +99702929.935,99.702930,-27.657604,-24.627469 +99703222.904,99.703223,-27.648779,-24.722212 +99703515.872,99.703516,-27.616682,-24.828918 +99703808.841,99.703809,-27.562109,-24.930543 +99704101.809,99.704102,-27.905859,-25.036269 +99704394.778,99.704395,-28.186213,-25.153323 +99704687.746,99.704688,-27.589783,-25.271610 +99704980.715,99.704981,-27.256584,-25.381613 +99705273.683,99.705274,-27.473860,-25.495524 +99705566.652,99.705567,-27.437586,-25.603143 +99705859.620,99.705860,-27.910770,-25.719581 +99706152.589,99.706153,-27.778713,-25.829623 +99706445.557,99.706446,-27.691362,-25.972391 +99706738.526,99.706739,-27.588339,-26.125636 +99707031.494,99.707031,-27.626076,-26.253679 +99707324.463,99.707324,-28.055859,-26.369659 +99707617.431,99.707617,-28.044283,-26.487597 +99707910.400,99.707910,-28.483065,-26.590534 +99708203.368,99.708203,-28.945368,-26.676365 +99708496.337,99.708496,-28.545469,-26.754013 +99708789.305,99.708789,-27.993605,-26.814847 +99709082.274,99.709082,-28.239389,-26.865132 +99709375.242,99.709375,-28.540644,-26.918529 +99709668.211,99.709668,-28.334522,-26.974950 +99709961.179,99.709961,-28.203115,-27.036777 +99710254.148,99.710254,-28.696159,-27.087890 +99710547.116,99.710547,-28.862097,-27.130446 +99710840.085,99.710840,-28.668015,-27.182830 +99711133.053,99.711133,-28.485704,-27.236118 +99711426.022,99.711426,-28.284590,-27.279522 +99711718.990,99.711719,-28.874155,-27.332093 +99712011.959,99.712012,-28.730221,-27.394026 +99712304.927,99.712305,-28.371201,-27.476341 +99712597.896,99.712598,-28.619726,-27.520649 +99712890.864,99.712891,-28.635715,-27.561203 +99713183.833,99.713184,-28.756866,-27.611500 +99713476.801,99.713477,-28.725246,-27.661012 +99713769.770,99.713770,-28.087158,-27.700447 +99714062.738,99.714063,-28.119509,-27.733600 +99714355.707,99.714356,-28.640467,-27.763632 +99714648.675,99.714649,-28.619871,-27.795941 +99714941.644,99.714942,-28.644363,-27.833560 +99715234.612,99.715235,-28.529768,-27.873781 +99715527.581,99.715528,-28.432257,-27.912130 +99715820.549,99.715821,-28.750376,-27.942740 +99716113.518,99.716114,-28.507906,-27.972306 +99716406.486,99.716406,-28.568102,-28.010361 +99716699.455,99.716699,-28.726610,-28.045037 +99716992.423,99.716992,-28.533148,-28.073481 +99717285.392,99.717285,-28.588007,-28.098017 +99717578.360,99.717578,-29.010603,-28.118058 +99717871.329,99.717871,-28.794039,-28.140374 +99718164.297,99.718164,-29.332388,-28.157637 +99718457.266,99.718457,-29.289270,-28.176393 +99718750.234,99.718750,-28.560835,-28.200345 +99719043.203,99.719043,-28.945391,-28.220187 +99719336.171,99.719336,-28.235847,-28.235624 +99719629.140,99.719629,-28.429832,-28.246430 +99719922.108,99.719922,-28.443138,-28.260870 +99720215.077,99.720215,-28.523142,-28.277609 +99720508.045,99.720508,-28.951757,-28.290735 +99720801.014,99.720801,-28.709379,-28.309867 +99721093.982,99.721094,-28.824785,-28.329088 +99721386.951,99.721387,-28.790602,-28.344204 +99721679.919,99.721680,-28.627760,-28.354214 +99721972.888,99.721973,-27.203754,-28.364509 +99722265.856,99.722266,-24.486586,-28.383849 +99722558.825,99.722559,-23.222570,-28.399086 +99722851.793,99.722852,-23.134491,-28.410761 +99723144.762,99.723145,-26.615656,-28.421835 +99723437.730,99.723438,-29.209528,-28.433679 +99723730.699,99.723731,-29.097301,-28.444896 +99724023.667,99.724024,-28.796581,-28.455288 +99724316.636,99.724317,-28.435844,-28.460234 +99724609.604,99.724610,-28.571615,-28.465314 +99724902.573,99.724903,-28.630527,-28.476149 +99725195.542,99.725196,-29.239042,-28.492563 +99725488.510,99.725489,-29.196199,-28.505314 +99725781.479,99.725781,-29.100092,-28.521373 +99726074.447,99.726074,-28.516827,-28.532435 +99726367.416,99.726367,-28.679522,-28.544516 +99726660.384,99.726660,-28.922598,-28.555459 +99726953.353,99.726953,-29.091238,-28.565294 +99727246.321,99.727246,-29.162886,-28.570655 +99727539.290,99.727539,-29.233210,-28.576635 +99727832.258,99.727832,-28.931437,-28.582599 +99728125.227,99.728125,-28.594007,-28.586158 +99728418.195,99.728418,-28.665415,-28.589389 +99728711.164,99.728711,-29.283428,-28.595433 +99729004.132,99.729004,-29.329247,-28.608325 +99729297.101,99.729297,-29.041819,-28.615539 +99729590.069,99.729590,-29.065519,-28.618736 +99729883.038,99.729883,-29.206110,-28.619798 +99730176.006,99.730176,-28.909616,-28.618044 +99730468.975,99.730469,-28.846287,-28.616341 +99730761.943,99.730762,-28.988047,-28.616898 +99731054.912,99.731055,-28.797592,-28.618215 +99731347.880,99.731348,-28.976549,-28.618698 +99731640.849,99.731641,-29.241102,-28.620097 +99731933.817,99.731934,-28.995108,-28.619187 +99732226.786,99.732227,-29.190908,-28.620394 +99732519.754,99.732520,-28.830843,-28.628010 +99732812.723,99.732813,-28.442892,-28.632262 +99733105.691,99.733106,-29.304790,-28.634410 +99733398.660,99.733399,-29.478874,-28.635776 +99733691.628,99.733692,-28.991941,-28.633977 +99733984.597,99.733985,-28.921291,-28.640254 +99734277.565,99.734278,-28.495062,-28.646569 +99734570.534,99.734571,-28.843161,-28.647882 +99734863.502,99.734864,-28.848513,-28.650629 +99735156.471,99.735156,-29.343004,-28.655244 +99735449.439,99.735449,-29.272196,-28.659441 +99735742.408,99.735742,-28.576874,-28.662846 +99736035.376,99.736035,-28.670439,-28.663614 +99736328.345,99.736328,-29.144770,-28.665052 +99736621.313,99.736621,-29.020624,-28.668080 +99736914.282,99.736914,-28.840963,-28.669733 +99737207.250,99.737207,-29.116386,-28.673805 +99737500.219,99.737500,-29.166410,-28.677933 +99737793.187,99.737793,-29.231453,-28.674806 +99738086.156,99.738086,-28.878269,-28.677735 +99738379.124,99.738379,-28.643881,-28.678024 +99738672.093,99.738672,-28.791222,-28.678336 +99738965.061,99.738965,-28.830126,-28.680917 +99739258.030,99.739258,-28.771841,-28.677852 +99739550.998,99.739551,-28.856001,-28.682528 +99739843.967,99.739844,-29.317179,-28.686192 +99740136.935,99.740137,-29.036545,-28.688912 +99740429.904,99.740430,-28.695320,-28.692265 +99740722.872,99.740723,-29.001612,-28.693940 +99741015.841,99.741016,-29.116159,-28.696131 +99741308.809,99.741309,-29.001381,-28.697610 +99741601.778,99.741602,-29.148449,-28.696361 +99741894.746,99.741895,-28.827219,-28.694383 +99742187.715,99.742188,-29.055231,-28.708058 +99742480.683,99.742481,-29.079288,-28.764555 +99742773.652,99.742774,-29.173939,-28.846142 +99743066.620,99.743067,-29.165583,-28.933722 +99743359.589,99.743360,-29.274609,-28.954599 +99743652.557,99.743653,-29.121170,-28.949514 +99743945.526,99.743946,-28.912287,-28.946432 +99744238.494,99.744238,-28.547804,-28.949635 +99744531.463,99.744531,-28.893835,-28.958094 +99744824.431,99.744824,-29.010612,-28.962320 +99745117.400,99.745117,-29.405308,-28.964681 +99745410.368,99.745410,-29.133892,-28.964557 +99745703.337,99.745703,-29.623209,-28.964197 +99745996.305,99.745996,-29.477198,-28.965609 +99746289.274,99.746289,-29.453142,-28.970299 +99746582.242,99.746582,-29.137882,-28.972149 +99746875.211,99.746875,-28.826408,-28.971038 +99747168.179,99.747168,-28.261641,-28.970017 +99747461.148,99.747461,-28.851643,-28.970701 +99747754.116,99.747754,-28.834253,-28.970388 +99748047.085,99.748047,-28.987656,-28.971428 +99748340.053,99.748340,-29.454748,-28.977100 +99748633.022,99.748633,-29.453123,-28.980283 +99748925.990,99.748926,-29.888231,-28.978335 +99749218.959,99.749219,-29.256849,-28.978836 +99749511.927,99.749512,-28.993856,-28.980560 +99749804.896,99.749805,-28.472919,-28.977976 +99750097.865,99.750098,-27.990112,-28.972973 +99750390.833,99.750391,-28.464788,-28.968677 +99750683.802,99.750684,-28.943632,-28.965660 +99750976.770,99.750977,-28.854548,-28.966971 +99751269.739,99.751270,-28.550320,-28.969315 +99751562.707,99.751563,-28.465803,-28.969789 +99751855.676,99.751856,-28.743834,-28.965348 +99752148.644,99.752149,-28.903311,-28.960724 +99752441.613,99.752442,-29.487450,-28.959521 +99752734.581,99.752735,-29.243597,-28.962071 +99753027.550,99.753028,-28.940634,-28.968076 +99753320.518,99.753321,-28.953718,-28.967919 +99753613.487,99.753613,-28.480530,-28.961320 +99753906.455,99.753906,-28.919817,-28.956448 +99754199.424,99.754199,-28.963259,-28.958563 +99754492.392,99.754492,-28.823910,-28.965825 +99754785.361,99.754785,-29.010740,-28.970359 +99755078.329,99.755078,-29.326252,-28.972559 +99755371.298,99.755371,-29.127279,-28.971165 +99755664.266,99.755664,-28.899134,-28.967345 +99755957.235,99.755957,-28.859034,-28.969229 +99756250.203,99.756250,-28.702211,-28.971760 +99756543.172,99.756543,-28.993891,-28.969486 +99756836.140,99.756836,-28.962387,-28.970944 +99757129.109,99.757129,-29.110125,-28.972913 +99757422.077,99.757422,-29.180937,-28.972628 +99757715.046,99.757715,-28.570879,-28.973447 +99758008.014,99.758008,-29.227230,-28.969815 +99758300.983,99.758301,-29.378750,-28.966209 +99758593.951,99.758594,-29.338715,-28.969423 +99758886.920,99.758887,-28.919353,-28.972134 +99759179.888,99.759180,-28.520407,-28.970751 +99759472.857,99.759473,-28.856518,-28.971293 +99759765.825,99.759766,-28.931030,-28.974029 +99760058.794,99.760059,-28.810438,-28.969503 +99760351.762,99.760352,-28.989277,-28.967239 +99760644.731,99.760645,-29.202452,-28.967518 +99760937.699,99.760938,-29.021479,-28.967487 +99761230.668,99.761231,-29.038620,-28.965764 +99761523.636,99.761524,-28.619169,-28.966315 +99761816.605,99.761817,-28.368929,-28.968770 +99762109.573,99.762110,-28.787443,-28.971495 +99762402.542,99.762403,-29.299795,-28.969914 +99762695.510,99.762696,-28.623867,-28.969139 +99762988.479,99.762988,-28.888580,-28.968288 +99763281.447,99.763281,-28.728861,-28.964586 +99763574.416,99.763574,-28.526840,-28.961385 +99763867.384,99.763867,-28.680939,-28.960072 +99764160.353,99.764160,-29.242529,-28.938659 +99764453.321,99.764453,-29.609566,-28.849809 +99764746.290,99.764746,-29.136267,-28.823874 +99765039.258,99.765039,-28.940859,-28.823485 +99765332.227,99.765332,-29.221069,-28.820349 +99765625.195,99.765625,-29.144423,-28.818739 +99765918.164,99.765918,-29.304440,-28.813612 +99766211.132,99.766211,-29.138323,-28.812273 +99766504.101,99.766504,-28.923239,-28.810308 +99766797.069,99.766797,-28.774687,-28.811883 +99767090.038,99.767090,-28.949743,-28.816305 +99767383.006,99.767383,-29.261904,-28.824953 +99767675.975,99.767676,-29.187868,-28.828536 +99767968.943,99.767969,-29.075098,-28.830927 +99768261.912,99.768262,-29.371550,-28.831426 +99768554.880,99.768555,-29.091280,-28.825927 +99768847.849,99.768848,-29.006294,-28.822354 +99769140.817,99.769141,-29.404249,-28.817357 +99769433.786,99.769434,-29.288225,-28.813013 +99769726.754,99.769727,-28.718744,-28.808318 +99770019.723,99.770020,-28.537481,-28.810795 +99770312.691,99.770313,-28.364944,-28.817280 +99770605.660,99.770606,-28.461771,-28.823078 +99770898.628,99.770899,-29.172392,-28.825921 +99771191.597,99.771192,-29.117929,-28.828665 +99771484.565,99.771485,-29.042133,-28.834824 +99771777.534,99.771778,-28.637115,-28.842328 +99772070.502,99.772071,-28.399696,-28.846006 +99772363.471,99.772363,-29.020391,-28.847038 +99772656.439,99.772656,-29.183998,-28.844467 +99772949.408,99.772949,-29.241419,-28.842620 +99773242.376,99.773242,-29.281559,-28.843970 +99773535.345,99.773535,-28.564608,-28.845311 +99773828.313,99.773828,-28.366665,-28.846817 +99774121.282,99.774121,-29.218468,-28.846313 +99774414.250,99.774414,-29.495352,-28.847323 +99774707.219,99.774707,-29.493067,-28.850097 +99775000.188,99.775000,-29.151876,-28.850251 +99775293.156,99.775293,-29.139822,-28.847572 +99775586.125,99.775586,-28.744394,-28.846856 +99775879.093,99.775879,-28.819412,-28.847156 +99776172.062,99.776172,-29.006693,-28.847664 +99776465.030,99.776465,-28.832041,-28.849183 +99776757.999,99.776758,-29.227495,-28.850781 +99777050.967,99.777051,-29.110991,-28.850769 +99777343.936,99.777344,-29.076233,-28.848348 +99777636.904,99.777637,-29.285318,-28.846529 +99777929.873,99.777930,-28.732851,-28.846370 +99778222.841,99.778223,-28.419468,-28.839322 +99778515.810,99.778516,-29.073175,-28.837147 +99778808.778,99.778809,-29.163029,-28.834081 +99779101.747,99.779102,-28.650442,-28.835790 +99779394.715,99.779395,-28.843390,-28.838663 +99779687.684,99.779688,-29.237219,-28.839037 +99779980.652,99.779981,-28.692764,-28.840748 +99780273.621,99.780274,-28.732414,-28.840562 +99780566.589,99.780567,-28.731455,-28.838448 +99780859.558,99.780860,-28.997335,-28.837777 +99781152.526,99.781153,-28.878452,-28.837453 +99781445.495,99.781445,-29.078037,-28.837790 +99781738.463,99.781738,-29.514170,-28.843146 +99782031.432,99.782031,-29.204273,-28.847669 +99782324.400,99.782324,-28.839815,-28.847039 +99782617.369,99.782617,-28.971762,-28.842035 +99782910.337,99.782910,-29.053442,-28.844874 +99783203.306,99.783203,-28.664646,-28.846513 +99783496.274,99.783496,-28.827234,-28.846039 +99783789.243,99.783789,-28.938536,-28.848838 +99784082.211,99.784082,-26.685728,-28.849668 +99784375.180,99.784375,-22.999327,-28.845603 +99784668.148,99.784668,-26.271862,-28.842544 +99784961.117,99.784961,-28.955286,-28.839282 +99785254.085,99.785254,-28.939878,-28.833044 +99785547.054,99.785547,-28.903009,-28.830236 +99785840.022,99.785840,-28.850370,-28.826749 +99786132.991,99.786133,-29.268421,-28.825263 +99786425.959,99.786426,-29.151775,-28.825776 +99786718.928,99.786719,-29.377096,-28.826085 +99787011.896,99.787012,-29.480829,-28.828596 +99787304.865,99.787305,-29.452755,-28.830281 +99787597.833,99.787598,-29.375566,-28.828249 +99787890.802,99.787891,-29.175215,-28.828459 +99788183.770,99.788184,-29.059183,-28.829134 +99788476.739,99.788477,-28.661560,-28.826880 +99788769.707,99.788770,-28.921278,-28.829877 +99789062.676,99.789063,-29.090727,-28.829687 +99789355.644,99.789356,-28.643404,-28.825513 +99789648.613,99.789649,-28.370012,-28.824381 +99789941.581,99.789942,-28.798868,-28.825676 +99790234.550,99.790235,-28.795191,-28.831674 +99790527.518,99.790528,-29.266291,-28.835495 +99790820.487,99.790820,-29.363737,-28.837635 +99791113.455,99.791113,-29.250257,-28.839020 +99791406.424,99.791406,-29.423294,-28.838387 +99791699.392,99.791699,-29.529604,-28.836324 +99791992.361,99.791992,-29.266457,-28.836654 +99792285.329,99.792285,-29.048931,-28.839788 +99792578.298,99.792578,-29.097219,-28.841129 +99792871.266,99.792871,-28.974686,-28.837398 +99793164.235,99.793164,-29.134056,-28.836463 +99793457.203,99.793457,-29.146284,-28.832540 +99793750.172,99.793750,-28.674398,-28.835977 +99794043.140,99.794043,-28.850216,-28.842539 +99794336.109,99.794336,-29.107782,-28.841646 +99794629.077,99.794629,-29.219162,-28.838337 +99794922.046,99.794922,-29.032759,-28.832992 +99795215.014,99.795215,-28.934711,-28.819163 +99795507.983,99.795508,-29.023937,-28.813454 +99795800.951,99.795801,-28.940962,-28.813916 +99796093.920,99.796094,-28.929319,-28.817340 +99796386.888,99.796387,-28.908308,-28.816653 +99796679.857,99.796680,-29.226307,-28.816288 +99796972.825,99.796973,-28.960627,-28.816481 +99797265.794,99.797266,-28.771515,-28.818688 +99797558.762,99.797559,-28.919855,-28.819619 +99797851.731,99.797852,-28.550453,-28.822199 +99798144.699,99.798145,-28.282808,-28.827302 +99798437.668,99.798438,-29.053762,-28.831308 +99798730.636,99.798731,-28.891087,-28.828375 +99799023.605,99.799024,-29.164757,-28.826952 +99799316.573,99.799317,-28.902674,-28.830527 +99799609.542,99.799610,-28.908251,-28.833318 +99799902.510,99.799903,-29.177303,-28.830061 +99800195.479,99.800195,-28.785284,-28.831143 +99800488.448,99.800488,-28.699324,-28.834148 +99800781.416,99.800781,-29.103577,-28.835090 +99801074.385,99.801074,-28.975510,-28.835007 +99801367.353,99.801367,-29.087154,-28.835891 +99801660.322,99.801660,-29.379398,-28.834100 +99801953.290,99.801953,-28.962992,-28.832049 +99802246.259,99.802246,-28.703203,-28.833054 +99802539.227,99.802539,-28.598301,-28.834543 +99802832.196,99.802832,-29.010321,-28.833431 +99803125.164,99.803125,-29.121489,-28.831000 +99803418.133,99.803418,-28.666105,-28.833334 +99803711.101,99.803711,-28.898422,-28.838392 +99804004.070,99.804004,-28.791712,-28.841142 +99804297.038,99.804297,-28.671804,-28.861177 +99804590.007,99.804590,-29.136452,-28.953273 +99804882.975,99.804883,-28.682480,-28.979399 +99805175.944,99.805176,-28.143373,-28.981065 +99805468.912,99.805469,-28.819376,-28.982015 +99805761.881,99.805762,-28.658838,-28.985447 +99806054.849,99.806055,-29.082865,-28.987477 +99806347.818,99.806348,-29.214624,-28.981512 +99806640.786,99.806641,-28.966696,-28.977150 +99806933.755,99.806934,-29.128555,-28.974186 +99807226.723,99.807227,-29.193850,-28.970161 +99807519.692,99.807520,-28.964693,-28.963386 +99807812.660,99.807813,-29.219355,-28.958598 +99808105.629,99.808106,-29.174084,-28.956558 +99808398.597,99.808399,-29.034920,-28.958358 +99808691.566,99.808692,-29.550997,-28.960860 +99808984.534,99.808985,-28.979321,-28.961166 +99809277.503,99.809278,-28.795834,-28.958554 +99809570.471,99.809570,-29.119047,-28.958104 +99809863.440,99.809863,-28.895323,-28.960108 +99810156.408,99.810156,-29.383234,-28.957460 +99810449.377,99.810449,-28.862318,-28.958476 +99810742.345,99.810742,-28.739300,-28.953102 +99811035.314,99.811035,-29.382326,-28.947675 +99811328.282,99.811328,-29.026489,-28.946515 +99811621.251,99.811621,-28.755484,-28.941562 +99811914.219,99.811914,-28.680592,-28.939167 +99812207.188,99.812207,-28.806200,-28.938473 +99812500.156,99.812500,-29.216181,-28.940806 +99812793.125,99.812793,-28.663126,-28.944959 +99813086.093,99.813086,-29.103045,-28.945761 +99813379.062,99.813379,-28.723333,-28.942671 +99813672.030,99.813672,-29.031403,-28.939070 +99813964.999,99.813965,-29.258528,-28.940387 +99814257.967,99.814258,-29.087048,-28.938729 +99814550.936,99.814551,-28.997843,-28.934972 +99814843.904,99.814844,-28.715208,-28.929751 +99815136.873,99.815137,-27.478025,-28.926149 +99815429.841,99.815430,-28.369629,-28.924978 +99815722.810,99.815723,-28.807211,-28.921770 +99816015.778,99.816016,-29.316286,-28.918495 +99816308.747,99.816309,-28.909372,-28.918762 +99816601.715,99.816602,-28.782227,-28.915966 +99816894.684,99.816895,-29.256628,-28.911890 +99817187.652,99.817188,-29.447176,-28.913384 +99817480.621,99.817481,-29.213661,-28.915919 +99817773.589,99.817774,-29.697763,-28.917996 +99818066.558,99.818067,-29.477633,-28.921044 +99818359.526,99.818360,-28.950033,-28.925458 +99818652.495,99.818652,-28.667690,-28.921200 +99818945.463,99.818945,-28.957340,-28.919928 +99819238.432,99.819238,-29.147923,-28.920339 +99819531.400,99.819531,-29.244886,-28.922733 +99819824.369,99.819824,-28.772963,-28.922118 +99820117.337,99.820117,-28.838892,-28.918904 +99820410.306,99.820410,-29.154503,-28.920095 +99820703.274,99.820703,-28.859285,-28.918190 +99820996.243,99.820996,-28.985640,-28.911039 +99821289.211,99.821289,-29.002495,-28.908004 +99821582.180,99.821582,-28.825953,-28.902692 +99821875.148,99.821875,-29.197399,-28.896614 +99822168.117,99.822168,-29.356861,-28.892435 +99822461.085,99.822461,-29.049082,-28.891299 +99822754.054,99.822754,-28.817293,-28.892727 +99823047.022,99.823047,-28.716431,-28.891212 +99823339.991,99.823340,-28.983648,-28.889508 +99823632.959,99.823633,-29.580654,-28.888762 +99823925.928,99.823926,-29.342012,-28.884209 +99824218.896,99.824219,-28.789043,-28.879826 +99824511.865,99.824512,-28.940985,-28.883791 +99824804.833,99.824805,-28.814529,-28.883084 +99825097.802,99.825098,-29.188330,-28.883700 +99825390.771,99.825391,-29.070692,-28.890120 +99825683.739,99.825684,-29.391016,-28.875079 +99825976.708,99.825977,-29.128620,-28.796906 +99826269.676,99.826270,-28.473425,-28.771004 +99826562.645,99.826563,-28.570601,-28.766216 +99826855.613,99.826856,-28.953192,-28.762300 +99827148.582,99.827149,-28.901291,-28.759288 +99827441.550,99.827442,-28.521961,-28.756963 +99827734.519,99.827735,-28.707886,-28.756406 +99828027.487,99.828027,-28.890934,-28.757780 +99828320.456,99.828320,-29.319250,-28.754099 +99828613.424,99.828613,-28.994013,-28.753951 +99828906.393,99.828906,-28.962978,-28.753654 +99829199.361,99.829199,-28.736757,-28.757603 +99829492.330,99.829492,-28.586546,-28.762181 +99829785.298,99.829785,-28.616575,-28.763796 +99830078.267,99.830078,-28.462486,-28.762387 +99830371.235,99.830371,-28.931496,-28.755975 +99830664.204,99.830664,-28.539728,-28.754048 +99830957.172,99.830957,-28.614454,-28.756464 +99831250.141,99.831250,-29.083118,-28.753918 +99831543.109,99.831543,-28.724955,-28.751715 +99831836.078,99.831836,-29.169086,-28.753391 +99832129.046,99.832129,-29.165186,-28.753178 +99832422.015,99.832422,-29.389782,-28.750815 +99832714.983,99.832715,-29.730650,-28.744628 +99833007.952,99.833008,-29.086855,-28.745811 +99833300.920,99.833301,-28.712831,-28.743020 +99833593.889,99.833594,-28.657677,-28.744414 +99833886.857,99.833887,-28.847555,-28.744652 +99834179.826,99.834180,-28.633211,-28.744369 +99834472.794,99.834473,-28.602846,-28.746375 +99834765.763,99.834766,-28.515310,-28.747698 +99835058.731,99.835059,-28.554533,-28.750151 +99835351.700,99.835352,-28.776922,-28.763522 +99835644.668,99.835645,-28.595881,-28.769735 +99835937.637,99.835938,-28.512064,-28.767737 +99836230.605,99.836231,-28.966173,-28.762268 +99836523.574,99.836524,-28.541927,-28.760363 +99836816.542,99.836817,-28.664179,-28.761248 +99837109.511,99.837110,-29.172712,-28.758285 +99837402.479,99.837402,-29.121279,-28.754111 +99837695.448,99.837695,-29.214441,-28.751380 +99837988.416,99.837988,-28.951971,-28.744746 +99838281.385,99.838281,-28.838711,-28.740767 +99838574.353,99.838574,-28.490721,-28.740016 +99838867.322,99.838867,-28.721350,-28.740972 +99839160.290,99.839160,-29.224810,-28.742032 +99839453.259,99.839453,-29.242102,-28.739846 +99839746.227,99.839746,-28.825140,-28.740130 +99840039.196,99.840039,-28.733633,-28.743538 +99840332.164,99.840332,-28.946413,-28.744120 +99840625.133,99.840625,-28.458055,-28.740060 +99840918.101,99.840918,-28.184729,-28.736095 +99841211.070,99.841211,-28.572664,-28.733103 +99841504.038,99.841504,-28.387655,-28.732836 +99841797.007,99.841797,-28.536762,-28.735475 +99842089.975,99.842090,-28.417171,-28.733144 +99842382.944,99.842383,-28.556780,-28.722888 +99842675.912,99.842676,-28.785038,-28.715385 +99842968.881,99.842969,-28.802061,-28.712402 +99843261.849,99.843262,-28.882036,-28.710634 +99843554.818,99.843555,-28.570072,-28.710478 +99843847.786,99.843848,-28.314169,-28.706468 +99844140.755,99.844141,-28.240057,-28.704492 +99844433.723,99.844434,-29.222925,-28.704973 +99844726.692,99.844727,-29.034941,-28.708393 +99845019.660,99.845020,-28.763924,-28.709566 +99845312.629,99.845313,-28.955919,-28.706476 +99845605.597,99.845606,-27.153311,-28.709071 +99845898.566,99.845899,-23.385796,-28.708820 +99846191.534,99.846192,-26.352133,-28.708561 +99846484.503,99.846485,-28.542521,-28.710530 +99846777.471,99.846777,-28.438084,-28.708213 +99847070.440,99.847070,-28.701204,-28.708701 +99847363.408,99.847363,-28.855270,-28.711957 +99847656.377,99.847656,-28.885393,-28.716272 +99847949.345,99.847949,-29.434004,-28.716961 +99848242.314,99.848242,-28.651503,-28.715746 +99848535.282,99.848535,-29.013288,-28.714067 +99848828.251,99.848828,-29.502468,-28.713470 +99849121.219,99.849121,-29.589848,-28.712023 +99849414.188,99.849414,-29.479342,-28.713580 +99849707.156,99.849707,-29.366020,-28.715397 +99850000.125,99.850000,-28.700848,-28.713925 +99850293.094,99.850293,-28.471748,-28.714202 +99850586.062,99.850586,-28.599777,-28.708798 +99850879.031,99.850879,-29.082193,-28.708624 +99851171.999,99.851172,-28.997042,-28.712864 +99851464.968,99.851465,-28.716415,-28.712860 +99851757.936,99.851758,-28.991495,-28.712892 +99852050.905,99.852051,-28.651983,-28.710897 +99852343.873,99.852344,-28.490194,-28.706788 +99852636.842,99.852637,-28.361887,-28.702719 +99852929.810,99.852930,-28.825052,-28.694293 +99853222.779,99.853223,-28.706499,-28.687995 +99853515.747,99.853516,-28.917622,-28.684499 +99853808.716,99.853809,-29.066431,-28.681520 +99854101.684,99.854102,-29.215078,-28.679825 +99854394.653,99.854395,-29.395012,-28.676400 +99854687.621,99.854688,-29.194366,-28.672997 +99854980.590,99.854981,-29.062090,-28.671441 +99855273.558,99.855274,-29.116144,-28.671070 +99855566.527,99.855567,-29.225477,-28.667027 +99855859.495,99.855859,-28.539497,-28.667996 +99856152.464,99.856152,-28.539413,-28.672947 +99856445.432,99.856445,-28.647451,-28.671961 +99856738.401,99.856738,-28.905811,-28.671865 +99857031.369,99.857031,-28.823814,-28.672714 +99857324.338,99.857324,-28.823622,-28.672138 +99857617.306,99.857617,-28.816338,-28.670322 +99857910.275,99.857910,-28.692606,-28.669272 +99858203.243,99.858203,-28.875666,-28.669572 +99858496.212,99.858496,-28.843407,-28.667534 +99858789.180,99.858789,-28.798422,-28.666475 +99859082.149,99.859082,-29.112738,-28.663887 +99859375.117,99.859375,-28.830921,-28.660301 +99859668.086,99.859668,-29.288725,-28.655129 +99859961.054,99.859961,-29.270763,-28.648710 +99860254.023,99.860254,-28.921120,-28.644547 +99860546.991,99.860547,-28.582369,-28.639592 +99860839.960,99.860840,-28.333788,-28.639947 +99861132.928,99.861133,-28.571701,-28.642945 +99861425.897,99.861426,-28.963749,-28.645967 +99861718.865,99.861719,-29.211864,-28.650736 +99862011.834,99.862012,-28.855839,-28.654044 +99862304.802,99.862305,-27.977238,-28.655013 +99862597.771,99.862598,-28.061403,-28.652205 +99862890.739,99.862891,-28.417662,-28.649406 +99863183.708,99.863184,-28.480539,-28.647436 +99863476.676,99.863477,-28.960859,-28.646624 +99863769.645,99.863770,-28.956223,-28.649488 +99864062.613,99.864063,-29.039469,-28.656190 +99864355.582,99.864356,-28.856691,-28.659147 +99864648.550,99.864649,-29.465967,-28.653630 +99864941.519,99.864942,-28.982410,-28.648078 +99865234.487,99.865234,-28.739548,-28.643866 +99865527.456,99.865527,-29.475351,-28.639967 +99865820.424,99.865820,-29.351057,-28.653132 +99866113.393,99.866113,-29.089682,-28.729119 +99866406.361,99.866406,-28.736717,-28.750892 +99866699.330,99.866699,-28.273537,-28.752982 +99866992.298,99.866992,-29.024487,-28.755363 +99867285.267,99.867285,-29.394512,-28.756259 +99867578.235,99.867578,-29.127857,-28.755554 +99867871.204,99.867871,-28.803053,-28.753981 +99868164.172,99.868164,-28.721117,-28.750264 +99868457.141,99.868457,-29.062624,-28.754871 +99868750.109,99.868750,-28.907528,-28.753731 +99869043.078,99.869043,-28.757984,-28.750497 +99869336.046,99.869336,-28.956709,-28.745912 +99869629.015,99.869629,-28.835199,-28.741430 +99869921.983,99.869922,-28.423843,-28.737805 +99870214.952,99.870215,-28.498407,-28.735152 +99870507.920,99.870508,-28.215096,-28.737214 +99870800.889,99.870801,-28.516874,-28.737192 +99871093.857,99.871094,-29.223356,-28.733117 +99871386.826,99.871387,-29.082521,-28.730777 +99871679.794,99.871680,-28.729334,-28.729239 +99871972.763,99.871973,-28.875668,-28.727945 +99872265.731,99.872266,-28.581003,-28.730686 +99872558.700,99.872559,-28.781706,-28.734128 +99872851.668,99.872852,-28.468969,-28.739559 +99873144.637,99.873145,-28.231680,-28.737188 +99873437.605,99.873438,-28.255888,-28.732309 +99873730.574,99.873731,-28.269712,-28.728641 +99874023.542,99.874024,-28.612759,-28.719906 +99874316.511,99.874317,-28.192024,-28.716018 +99874609.479,99.874609,-28.166927,-28.713559 +99874902.448,99.874902,-28.314325,-28.711971 +99875195.417,99.875195,-28.505424,-28.712765 +99875488.385,99.875488,-28.243250,-28.710691 +99875781.354,99.875781,-28.728390,-28.704804 +99876074.322,99.876074,-29.220999,-28.706274 +99876367.291,99.876367,-28.823875,-28.709587 +99876660.259,99.876660,-28.529167,-28.712700 +99876953.228,99.876953,-28.781862,-28.714405 +99877246.196,99.877246,-29.085140,-28.713703 +99877539.165,99.877539,-28.853758,-28.712980 +99877832.133,99.877832,-29.054346,-28.715325 +99878125.102,99.878125,-28.996084,-28.716759 +99878418.070,99.878418,-28.557535,-28.716687 +99878711.039,99.878711,-28.353621,-28.717970 +99879004.007,99.879004,-28.376299,-28.721003 +99879296.976,99.879297,-28.698725,-28.720991 +99879589.944,99.879590,-28.498838,-28.716311 +99879882.913,99.879883,-27.994699,-28.711337 +99880175.881,99.880176,-28.187561,-28.707830 +99880468.850,99.880469,-28.273308,-28.704149 +99880761.818,99.880762,-28.504948,-28.704932 +99881054.787,99.881055,-28.571173,-28.706756 +99881347.755,99.881348,-29.000277,-28.708537 +99881640.724,99.881641,-29.051125,-28.707743 +99881933.692,99.881934,-29.002068,-28.699984 +99882226.661,99.882227,-28.544748,-28.698096 +99882519.629,99.882520,-28.195963,-28.707887 +99882812.598,99.882813,-28.406466,-28.715752 +99883105.566,99.883106,-28.531181,-28.717096 +99883398.535,99.883399,-28.766058,-28.720643 +99883691.503,99.883692,-28.973738,-28.718976 +99883984.472,99.883984,-29.259205,-28.714723 +99884277.440,99.884277,-28.624586,-28.711559 +99884570.409,99.884570,-28.437294,-28.710208 +99884863.377,99.884863,-28.274519,-28.706306 +99885156.346,99.885156,-28.208178,-28.704321 +99885449.314,99.885449,-28.416634,-28.699723 +99885742.283,99.885742,-28.668167,-28.689771 +99886035.251,99.886035,-28.710554,-28.683059 +99886328.220,99.886328,-28.544216,-28.683732 +99886621.188,99.886621,-28.824331,-28.686059 +99886914.157,99.886914,-28.752460,-28.687755 +99887207.125,99.887207,-28.824179,-28.679247 +99887500.094,99.887500,-28.757486,-28.636382 +99887793.062,99.887793,-28.668884,-28.619999 +99888086.031,99.888086,-28.875677,-28.622432 +99888378.999,99.888379,-29.316462,-28.625407 +99888671.968,99.888672,-28.850689,-28.625148 +99888964.936,99.888965,-29.005360,-28.622459 +99889257.905,99.889258,-28.887430,-28.622125 +99889550.873,99.889551,-28.806755,-28.621316 +99889843.842,99.889844,-28.827171,-28.622653 +99890136.810,99.890137,-28.354534,-28.625237 +99890429.779,99.890430,-28.746122,-28.626338 +99890722.747,99.890723,-28.596825,-28.629250 +99891015.716,99.891016,-28.516047,-28.627270 +99891308.684,99.891309,-28.668938,-28.619962 +99891601.653,99.891602,-28.511288,-28.616958 +99891894.621,99.891895,-28.807175,-28.617864 +99892187.590,99.892188,-29.037474,-28.616543 +99892480.558,99.892481,-28.960785,-28.617658 +99892773.527,99.892774,-29.104332,-28.620926 +99893066.495,99.893066,-28.505739,-28.624696 +99893359.464,99.893359,-28.088602,-28.627481 +99893652.432,99.893652,-28.422323,-28.629971 +99893945.401,99.893945,-27.933216,-28.636567 +99894238.369,99.894238,-28.655445,-28.641758 +99894531.338,99.894531,-29.017900,-28.646896 +99894824.306,99.894824,-28.957829,-28.650443 +99895117.275,99.895117,-29.181627,-28.654935 +99895410.243,99.895410,-28.814880,-28.656425 +99895703.212,99.895703,-28.400528,-28.660948 +99895996.180,99.895996,-28.737778,-28.659410 +99896289.149,99.896289,-28.999517,-28.657255 +99896582.117,99.896582,-29.089489,-28.656184 +99896875.086,99.896875,-29.157034,-28.656164 +99897168.054,99.897168,-28.726294,-28.655677 +99897461.023,99.897461,-28.723188,-28.654650 +99897753.991,99.897754,-29.158453,-28.655419 +99898046.960,99.898047,-28.892530,-28.651350 +99898339.928,99.898340,-28.865398,-28.648999 +99898632.897,99.898633,-29.028227,-28.651837 +99898925.865,99.898926,-29.243700,-28.656648 +99899218.834,99.899219,-29.110817,-28.658358 +99899511.802,99.899512,-28.218359,-28.655146 +99899804.771,99.899805,-28.573162,-28.654733 +99900097.740,99.900098,-28.755821,-28.655999 +99900390.708,99.900391,-28.421146,-28.656509 +99900683.677,99.900684,-28.687830,-28.657726 +99900976.645,99.900977,-28.569511,-28.660673 +99901269.614,99.901270,-28.814775,-28.664041 +99901562.582,99.901563,-28.849892,-28.661654 +99901855.551,99.901856,-28.156393,-28.661069 +99902148.519,99.902149,-28.595587,-28.659155 +99902441.488,99.902441,-29.292696,-28.658000 +99902734.456,99.902734,-29.105007,-28.660153 +99903027.425,99.903027,-28.592966,-28.663363 +99903320.393,99.903320,-28.966810,-28.665664 +99903613.362,99.903613,-28.725893,-28.666004 +99903906.330,99.903906,-28.380426,-28.665628 +99904199.299,99.904199,-28.595675,-28.661183 +99904492.267,99.904492,-28.669409,-28.661069 +99904785.236,99.904785,-28.871986,-28.660592 +99905078.204,99.905078,-28.701860,-28.661203 +99905371.173,99.905371,-28.146574,-28.663198 +99905664.141,99.905664,-28.089962,-28.665503 +99905957.110,99.905957,-28.393206,-28.668965 +99906250.078,99.906250,-29.192204,-28.671543 +99906543.047,99.906543,-29.071821,-28.673392 +99906836.015,99.906836,-28.490175,-28.672981 +99907128.984,99.907129,-27.917500,-28.672092 +99907421.952,99.907422,-25.241186,-28.672718 +99907714.921,99.907715,-27.141703,-28.672371 +99908007.889,99.908008,-29.165300,-28.673075 +99908300.858,99.908301,-29.159393,-28.673124 +99908593.826,99.908594,-29.023504,-28.668192 +99908886.795,99.908887,-28.531038,-28.664974 +99909179.763,99.909180,-28.711073,-28.661584 +99909472.732,99.909473,-28.838598,-28.659010 +99909765.700,99.909766,-29.031969,-28.657206 +99910058.669,99.910059,-28.775761,-28.656254 +99910351.637,99.910352,-28.647470,-28.660111 +99910644.606,99.910645,-28.593920,-28.658496 +99910937.574,99.910938,-28.260256,-28.658192 +99911230.543,99.911231,-28.203518,-28.658503 +99911523.511,99.911524,-28.647093,-28.659009 +99911816.480,99.911816,-28.858667,-28.658656 +99912109.448,99.912109,-28.687712,-28.654813 +99912402.417,99.912402,-28.735153,-28.652670 +99912695.385,99.912695,-29.273399,-28.652286 +99912988.354,99.912988,-28.998455,-28.648184 +99913281.322,99.913281,-28.594837,-28.647195 +99913574.291,99.913574,-28.580896,-28.651090 +99913867.259,99.913867,-29.192184,-28.653745 +99914160.228,99.914160,-29.384178,-28.658034 +99914453.196,99.914453,-28.877714,-28.654189 +99914746.165,99.914746,-28.625170,-28.652230 +99915039.133,99.915039,-28.924494,-28.651616 +99915332.102,99.915332,-28.707275,-28.649548 +99915625.070,99.915625,-28.846418,-28.648216 +99915918.039,99.915918,-28.519373,-28.646896 +99916211.007,99.916211,-28.897297,-28.646050 +99916503.976,99.916504,-28.674063,-28.646770 +99916796.944,99.916797,-28.526506,-28.644730 +99917089.913,99.917090,-28.713726,-28.641280 +99917382.881,99.917383,-28.932497,-28.639028 +99917675.850,99.917676,-28.965513,-28.638379 +99917968.818,99.917969,-28.482294,-28.627298 +99918261.787,99.918262,-28.660624,-28.556074 +99918554.755,99.918555,-28.955883,-28.531169 +99918847.724,99.918848,-29.016800,-28.529507 +99919140.692,99.919141,-28.601599,-28.525244 +99919433.661,99.919434,-28.275698,-28.523552 +99919726.629,99.919727,-28.444511,-28.531056 +99920019.598,99.920020,-28.146278,-28.533751 +99920312.566,99.920313,-28.250765,-28.535307 +99920605.535,99.920606,-28.428776,-28.538855 +99920898.503,99.920899,-28.913136,-28.539737 +99921191.472,99.921191,-29.048269,-28.541229 +99921484.440,99.921484,-28.660444,-28.542689 +99921777.409,99.921777,-28.964396,-28.542091 +99922070.377,99.922070,-28.727262,-28.544469 +99922363.346,99.922363,-28.393198,-28.545227 +99922656.314,99.922656,-28.469574,-28.541188 +99922949.283,99.922949,-28.842287,-28.539953 +99923242.251,99.923242,-28.848185,-28.540821 +99923535.220,99.923535,-28.814020,-28.540720 +99923828.188,99.923828,-28.918768,-28.540530 +99924121.157,99.924121,-28.611481,-28.541556 +99924414.125,99.924414,-28.609079,-28.543008 +99924707.094,99.924707,-28.375658,-28.544788 +99925000.062,99.925000,-28.351789,-28.543540 +99925293.031,99.925293,-28.461594,-28.543831 +99925586.000,99.925586,-28.725691,-28.550031 +99925878.968,99.925879,-29.170599,-28.559280 +99926171.937,99.926172,-29.082518,-28.565542 +99926464.905,99.926465,-28.797323,-28.561482 +99926757.874,99.926758,-28.766390,-28.558213 +99927050.842,99.927051,-28.630163,-28.560195 +99927343.811,99.927344,-28.913996,-28.568232 +99927636.779,99.927637,-28.709251,-28.608392 +99927929.748,99.927930,-28.766270,-28.625177 +99928222.716,99.928223,-28.882753,-28.625119 +99928515.685,99.928516,-28.595697,-28.621692 +99928808.653,99.928809,-28.413490,-28.618332 +99929101.622,99.929102,-28.529844,-28.616849 +99929394.590,99.929395,-28.530985,-28.616019 +99929687.559,99.929688,-28.558226,-28.613030 +99929980.527,99.929981,-28.693554,-28.609034 +99930273.496,99.930273,-28.877584,-28.607716 +99930566.464,99.930566,-28.526068,-28.605712 +99930859.433,99.930859,-28.555914,-28.602066 +99931152.401,99.931152,-28.557421,-28.601440 +99931445.370,99.931445,-28.739065,-28.603603 +99931738.338,99.931738,-28.464834,-28.600856 +99932031.307,99.932031,-28.293631,-28.599681 +99932324.275,99.932324,-28.728086,-28.601979 +99932617.244,99.932617,-28.904703,-28.602257 +99932910.212,99.932910,-28.521282,-28.596233 +99933203.181,99.933203,-28.376545,-28.593144 +99933496.149,99.933496,-28.584890,-28.592498 +99933789.118,99.933789,-28.781870,-28.593588 +99934082.086,99.934082,-28.461618,-28.591052 +99934375.055,99.934375,-28.158344,-28.588579 +99934668.023,99.934668,-28.735312,-28.586252 +99934960.992,99.934961,-28.868578,-28.583845 +99935253.960,99.935254,-28.872713,-28.583172 +99935546.929,99.935547,-28.629251,-28.585498 +99935839.897,99.935840,-28.232969,-28.586001 +99936132.866,99.936133,-28.620953,-28.590513 +99936425.834,99.936426,-29.107944,-28.591267 +99936718.803,99.936719,-28.790369,-28.593989 +99937011.771,99.937012,-28.655191,-28.596406 +99937304.740,99.937305,-28.422483,-28.595280 +99937597.708,99.937598,-28.633467,-28.592225 +99937890.677,99.937891,-27.714132,-28.589585 +99938183.645,99.938184,-23.567566,-28.588261 +99938476.614,99.938477,-26.203001,-28.589416 +99938769.582,99.938770,-28.780085,-28.590071 +99939062.551,99.939063,-28.604424,-28.589058 +99939355.519,99.939356,-28.853296,-28.589406 +99939648.488,99.939648,-29.299900,-28.591913 +99939941.456,99.939941,-28.962866,-28.593505 +99940234.425,99.940234,-28.986109,-28.595268 +99940527.393,99.940527,-28.922289,-28.597526 +99940820.362,99.940820,-28.814594,-28.600815 +99941113.330,99.941113,-28.780441,-28.598281 +99941406.299,99.941406,-29.033165,-28.592384 +99941699.267,99.941699,-28.762722,-28.594194 +99941992.236,99.941992,-28.465124,-28.596604 +99942285.204,99.942285,-28.702005,-28.596885 +99942578.173,99.942578,-28.680346,-28.602197 +99942871.141,99.942871,-28.916573,-28.605238 +99943164.110,99.943164,-28.714937,-28.604603 +99943457.078,99.943457,-28.951683,-28.602420 +99943750.047,99.943750,-28.698769,-28.600820 +99944043.015,99.944043,-28.518032,-28.596952 +99944335.984,99.944336,-28.801844,-28.595070 +99944628.952,99.944629,-28.927954,-28.591298 +99944921.921,99.944922,-28.691425,-28.590915 +99945214.889,99.945215,-28.743448,-28.590061 +99945507.858,99.945508,-28.999672,-28.586421 +99945800.826,99.945801,-29.410452,-28.582572 +99946093.795,99.946094,-29.308323,-28.576206 +99946386.763,99.946387,-28.592571,-28.572187 +99946679.732,99.946680,-28.595079,-28.572244 +99946972.700,99.946973,-28.766041,-28.571320 +99947265.669,99.947266,-28.988010,-28.571127 +99947558.637,99.947559,-29.072828,-28.569853 +99947851.606,99.947852,-29.196878,-28.572199 +99948144.574,99.948145,-29.156284,-28.572812 +99948437.543,99.948438,-28.658262,-28.574183 +99948730.511,99.948731,-28.545732,-28.571568 +99949023.480,99.949023,-28.336353,-28.526312 +99949316.448,99.949316,-28.596285,-28.507667 +99949609.417,99.949609,-28.427519,-28.510025 +99949902.385,99.949902,-28.467127,-28.512440 +99950195.354,99.950195,-28.592100,-28.509863 +99950488.323,99.950488,-28.378851,-28.507504 +99950781.291,99.950781,-28.122107,-28.508435 +99951074.260,99.951074,-28.181763,-28.504797 +99951367.228,99.951367,-28.482492,-28.500474 +99951660.197,99.951660,-28.282644,-28.499264 +99951953.165,99.951953,-28.691114,-28.501255 +99952246.134,99.952246,-29.021349,-28.504032 +99952539.102,99.952539,-28.774590,-28.501392 +99952832.071,99.952832,-28.402969,-28.496971 +99953125.039,99.953125,-28.557238,-28.497974 +99953418.008,99.953418,-28.507072,-28.499508 +99953710.976,99.953711,-28.732433,-28.499735 +99954003.945,99.954004,-28.810675,-28.496693 +99954296.913,99.954297,-28.995325,-28.499863 +99954589.882,99.954590,-28.549500,-28.502682 +99954882.850,99.954883,-28.304531,-28.499053 +99955175.819,99.955176,-28.825932,-28.497698 +99955468.787,99.955469,-29.048002,-28.496893 +99955761.756,99.955762,-28.920197,-28.493733 +99956054.724,99.956055,-29.174969,-28.495389 +99956347.693,99.956348,-29.009525,-28.497297 +99956640.661,99.956641,-29.071686,-28.491426 +99956933.630,99.956934,-28.865292,-28.484627 +99957226.598,99.957227,-28.558048,-28.480108 +99957519.567,99.957520,-28.502085,-28.479519 +99957812.535,99.957813,-28.588095,-28.476466 +99958105.504,99.958106,-28.308901,-28.481018 +99958398.472,99.958398,-28.824541,-28.547251 +99958691.441,99.958691,-29.054558,-28.571535 +99958984.409,99.958984,-28.866312,-28.570652 +99959277.378,99.959277,-28.649679,-28.566404 +99959570.346,99.959570,-28.607485,-28.561500 +99959863.315,99.959863,-28.660633,-28.554746 +99960156.283,99.960156,-28.369762,-28.549376 +99960449.252,99.960449,-28.546232,-28.545011 +99960742.220,99.960742,-28.885273,-28.539871 +99961035.189,99.961035,-28.555202,-28.535715 +99961328.157,99.961328,-28.233088,-28.530382 +99961621.126,99.961621,-28.919891,-28.521535 +99961914.094,99.961914,-29.339468,-28.498936 +99962207.063,99.962207,-28.767218,-28.477222 +99962500.031,99.962500,-29.149252,-28.476403 +99962793.000,99.962793,-28.893599,-28.472069 +99963085.968,99.963086,-28.751415,-28.464346 +99963378.937,99.963379,-28.542759,-28.457540 +99963671.905,99.963672,-28.589712,-28.449053 +99963964.874,99.963965,-28.382772,-28.444994 +99964257.842,99.964258,-28.360161,-28.444543 +99964550.811,99.964551,-28.119068,-28.439335 +99964843.779,99.964844,-28.326017,-28.430487 +99965136.748,99.965137,-28.242487,-28.419997 +99965429.716,99.965430,-28.002405,-28.412170 +99965722.685,99.965723,-28.212475,-28.404787 +99966015.653,99.966016,-28.271255,-28.392276 +99966308.622,99.966309,-28.503799,-28.381881 +99966601.590,99.966602,-28.805567,-28.375823 +99966894.559,99.966895,-28.635920,-28.368812 +99967187.527,99.967188,-28.603535,-28.356646 +99967480.496,99.967480,-28.729019,-28.341455 +99967773.464,99.967773,-29.054512,-28.330003 +99968066.433,99.968066,-28.854990,-28.308963 +99968359.401,99.968359,-29.089184,-28.281342 +99968652.370,99.968652,-28.249668,-28.268730 +99968945.338,99.968945,-24.640856,-28.257892 +99969238.307,99.969238,-26.516108,-28.248430 +99969531.275,99.969531,-28.868422,-28.240549 +99969824.244,99.969824,-28.906292,-28.194253 +99970117.212,99.970117,-28.340113,-28.125499 +99970410.181,99.970410,-28.539324,-28.110118 +99970703.149,99.970703,-28.656044,-28.098188 +99970996.118,99.970996,-28.079020,-28.088426 +99971289.086,99.971289,-27.995584,-28.078335 +99971582.055,99.971582,-28.567347,-28.061883 +99971875.023,99.971875,-28.744123,-28.055055 +99972167.992,99.972168,-28.672333,-28.048398 +99972460.960,99.972461,-28.362961,-28.033849 +99972753.929,99.972754,-28.285889,-28.017861 +99973046.897,99.973047,-28.661766,-28.006081 +99973339.866,99.973340,-28.585865,-27.994110 +99973632.834,99.973633,-28.616690,-27.975698 +99973925.803,99.973926,-28.358320,-27.959397 +99974218.771,99.974219,-28.915131,-27.946529 +99974511.740,99.974512,-28.530754,-27.931514 +99974804.708,99.974805,-28.239912,-27.919653 +99975097.677,99.975098,-28.671011,-27.905903 +99975390.646,99.975391,-28.754011,-27.892636 +99975683.614,99.975684,-28.204311,-27.880972 +99975976.583,99.975977,-28.452044,-27.864293 +99976269.551,99.976270,-28.898680,-27.841913 +99976562.520,99.976563,-28.268145,-27.821845 +99976855.488,99.976855,-27.891571,-27.807549 +99977148.457,99.977148,-28.054440,-27.792670 +99977441.425,99.977441,-28.343592,-27.777108 +99977734.394,99.977734,-28.220285,-27.759759 +99978027.362,99.978027,-28.271275,-27.741219 +99978320.331,99.978320,-28.367964,-27.722394 +99978613.299,99.978613,-28.762085,-27.701257 +99978906.268,99.978906,-28.654985,-27.677978 +99979199.236,99.979199,-28.054052,-27.658150 +99979492.205,99.979492,-28.188765,-27.640753 +99979785.173,99.979785,-28.321184,-27.615280 +99980078.142,99.980078,-28.221275,-27.600966 +99980371.110,99.980371,-28.370306,-27.589477 +99980664.079,99.980664,-28.214657,-27.571278 +99980957.047,99.980957,-28.246670,-27.545163 +99981250.016,99.981250,-28.068604,-27.514062 +99981542.984,99.981543,-27.848286,-27.489626 +99981835.953,99.981836,-26.318592,-27.464355 +99982128.921,99.982129,-26.208475,-27.434213 +99982421.890,99.982422,-28.585457,-27.400907 +99982714.858,99.982715,-28.098490,-27.362042 +99983007.827,99.983008,-27.878105,-27.332008 +99983300.795,99.983301,-27.824255,-27.297164 +99983593.764,99.983594,-27.811266,-27.250418 +99983886.732,99.983887,-28.146204,-27.141829 +99984179.701,99.984180,-28.455687,-27.068219 +99984472.669,99.984473,-28.088671,-27.038045 +99984765.638,99.984766,-27.746319,-26.990646 +99985058.606,99.985059,-27.382246,-26.949938 +99985351.575,99.985352,-27.718340,-26.919982 +99985644.543,99.985645,-27.972277,-26.886192 +99985937.512,99.985938,-27.654791,-26.838993 +99986230.480,99.986230,-27.830242,-26.789234 +99986523.449,99.986523,-27.798119,-26.754513 +99986816.417,99.986816,-27.686064,-26.715247 +99987109.386,99.987109,-27.239714,-26.679161 +99987402.354,99.987402,-27.070246,-26.643371 +99987695.323,99.987695,-27.525379,-26.603365 +99987988.291,99.987988,-26.614536,-26.559285 +99988281.260,99.988281,-26.012503,-26.519603 +99988574.228,99.988574,-27.091188,-26.472305 +99988867.197,99.988867,-27.194796,-26.432975 +99989160.165,99.989160,-27.182770,-26.399110 +99989453.134,99.989453,-27.559652,-26.345146 +99989746.102,99.989746,-24.391443,-26.294804 +99990039.071,99.990039,-23.245415,-26.238792 +99990332.039,99.990332,-26.713610,-26.177548 +99990625.008,99.990625,-26.914715,-26.112463 +99990917.976,99.990918,-26.949436,-26.033371 +99991210.945,99.991211,-26.957590,-25.955388 +99991503.913,99.991504,-26.521606,-25.893203 +99991796.882,99.991797,-27.394808,-25.834847 +99992089.850,99.992090,-27.745441,-25.756583 +99992382.819,99.992383,-27.048033,-25.656025 +99992675.787,99.992676,-26.735464,-25.558987 +99992968.756,99.992969,-26.921152,-25.457817 +99993261.724,99.993262,-27.006018,-25.365220 +99993554.693,99.993555,-26.319012,-25.258579 +99993847.661,99.993848,-26.655432,-25.138702 +99994140.630,99.994141,-27.065567,-25.025474 +99994433.598,99.994434,-26.943016,-24.922893 +99994726.567,99.994727,-26.990232,-24.814897 +99995019.535,99.995020,-26.617500,-24.677073 +99995312.504,99.995313,-27.012983,-24.531250 +99995605.472,99.995605,-27.343235,-24.421553 +99995898.441,99.995898,-26.693941,-24.302074 +99996191.409,99.996191,-26.258730,-24.177069 +99996484.378,99.996484,-26.384432,-24.043363 +99996777.346,99.996777,-27.024027,-23.902011 +99997070.315,99.997070,-26.824633,-23.753791 +99997363.283,99.997363,-26.553434,-23.623632 +99997656.252,99.997656,-26.332466,-23.518215 +99997949.220,99.997949,-26.260069,-23.417773 +99998242.189,99.998242,-26.060375,-23.301317 +99998535.157,99.998535,-26.125898,-23.211633 +99998828.126,99.998828,-26.032301,-23.093531 +99999121.094,99.999121,-26.255512,-22.971898 +99999414.063,99.999414,-26.368393,-22.845278 +99999707.031,99.999707,-25.582342,-22.739664 +100000000.000,100.000000,-26.686876,-22.623764 +100000292.969,100.000293,-26.806696,-22.490088 +100000585.937,100.000586,-26.198372,-22.341828 +100000878.906,100.000879,-25.625502,-22.186784 +100001171.874,100.001172,-25.048033,-22.016227 +100001464.843,100.001465,-25.417749,-21.848780 +100001757.811,100.001758,-25.674240,-21.683809 +100002050.780,100.002051,-25.413698,-21.497097 +100002343.748,100.002344,-24.904682,-21.315749 +100002636.717,100.002637,-24.613106,-21.169171 +100002929.685,100.002930,-25.177893,-21.029811 +100003222.654,100.003223,-24.719900,-20.861407 +100003515.622,100.003516,-23.807825,-20.706113 +100003808.591,100.003809,-21.014503,-20.509747 +100004101.559,100.004102,-22.237593,-20.300712 +100004394.528,100.004395,-24.769251,-20.104350 +100004687.496,100.004687,-23.452879,-19.928205 +100004980.465,100.004980,-23.925295,-19.751385 +100005273.433,100.005273,-24.668972,-19.563481 +100005566.402,100.005566,-24.249525,-19.357290 +100005859.370,100.005859,-23.395639,-19.119658 +100006152.339,100.006152,-23.225885,-18.921111 +100006445.307,100.006445,-24.300928,-18.736861 +100006738.276,100.006738,-24.041103,-18.498677 +100007031.244,100.007031,-24.197227,-18.269382 +100007324.213,100.007324,-24.185392,-18.057339 +100007617.181,100.007617,-23.886881,-17.879420 +100007910.150,100.007910,-23.666870,-17.732345 +100008203.118,100.008203,-23.894701,-17.555088 +100008496.087,100.008496,-23.394474,-17.340268 +100008789.055,100.008789,-23.660902,-17.044649 +100009082.024,100.009082,-22.311285,-16.770314 +100009374.992,100.009375,-22.107431,-16.493633 +100009667.961,100.009668,-23.007378,-16.228495 +100009960.929,100.009961,-22.627905,-15.983079 +100010253.898,100.010254,-22.152874,-15.738163 +100010546.866,100.010547,-21.946516,-15.488778 +100010839.835,100.010840,-21.234612,-15.218078 +100011132.803,100.011133,-21.110537,-14.926327 +100011425.772,100.011426,-21.804180,-14.636997 +100011718.740,100.011719,-22.102823,-14.402527 +100012011.709,100.012012,-21.059265,-14.195985 +100012304.677,100.012305,-20.050962,-13.995550 +100012597.646,100.012598,-20.054893,-13.798525 +100012890.614,100.012891,-19.804075,-13.596389 +100013183.583,100.013184,-20.104164,-13.389765 +100013476.551,100.013477,-19.474594,-13.223285 +100013769.520,100.013770,-18.928190,-13.029467 +100014062.488,100.014062,-19.015879,-12.812206 +100014355.457,100.014355,-19.363655,-12.628146 +100014648.425,100.014648,-19.028744,-12.455009 +100014941.394,100.014941,-17.937902,-12.286772 +100015234.362,100.015234,-17.621180,-12.138546 +100015527.331,100.015527,-18.639893,-11.977329 +100015820.299,100.015820,-18.148039,-11.841487 +100016113.268,100.016113,-17.882143,-11.724995 +100016406.236,100.016406,-17.530107,-11.595742 +100016699.205,100.016699,-17.132202,-11.481746 +100016992.173,100.016992,-16.779358,-11.389497 +100017285.142,100.017285,-17.183661,-11.294762 +100017578.110,100.017578,-17.937639,-11.188053 +100017871.079,100.017871,-18.023140,-11.094691 +100018164.047,100.018164,-17.342798,-11.009802 +100018457.016,100.018457,-18.290768,-10.914653 +100018749.984,100.018750,-17.131443,-10.818199 +100019042.953,100.019043,-16.893120,-10.729923 +100019335.921,100.019336,-16.573675,-10.659756 +100019628.890,100.019629,-17.204952,-10.577503 +100019921.858,100.019922,-16.738693,-10.486860 +100020214.827,100.020215,-16.036016,-10.406470 +100020507.795,100.020508,-15.486626,-10.328181 +100020800.764,100.020801,-15.150233,-10.240921 +100021093.732,100.021094,-14.601387,-10.168348 +100021386.701,100.021387,-14.508422,-10.104458 +100021679.669,100.021680,-14.401636,-10.045873 +100021972.638,100.021973,-13.647635,-9.978913 +100022265.606,100.022266,-13.587372,-9.911934 +100022558.575,100.022559,-14.422423,-9.839675 +100022851.543,100.022852,-14.475756,-9.760055 +100023144.512,100.023145,-13.529873,-9.697916 +100023437.480,100.023437,-13.710807,-9.633271 +100023730.449,100.023730,-12.556067,-9.562179 +100024023.417,100.024023,-12.103869,-9.500665 +100024316.386,100.024316,-12.178640,-9.437999 +100024609.354,100.024609,-12.447784,-9.377935 +100024902.323,100.024902,-12.250531,-9.308359 +100025195.292,100.025195,-11.806390,-9.248836 +100025488.260,100.025488,-11.229331,-9.196544 +100025781.229,100.025781,-10.412666,-9.150420 +100026074.197,100.026074,-10.958099,-9.094231 +100026367.166,100.026367,-11.092174,-9.029789 +100026660.134,100.026660,-9.789506,-8.971690 +100026953.103,100.026953,-9.720281,-8.923454 +100027246.071,100.027246,-9.829741,-8.873626 +100027539.040,100.027539,-10.382371,-8.826917 +100027832.008,100.027832,-11.042213,-8.775921 +100028124.977,100.028125,-10.070697,-8.723299 +100028417.945,100.028418,-9.048837,-8.670044 +100028710.914,100.028711,-7.447539,-8.617284 +100029003.882,100.029004,-7.487874,-8.562653 +100029296.851,100.029297,-7.178476,-8.512428 +100029589.819,100.029590,-7.097086,-8.458307 +100029882.788,100.029883,-7.134363,-8.412184 +100030175.756,100.030176,-6.879075,-8.375029 +100030468.725,100.030469,-6.612182,-8.340276 +100030761.693,100.030762,-6.003119,-8.295921 +100031054.662,100.031055,-5.401394,-8.252265 +100031347.630,100.031348,-5.148821,-8.208730 +100031640.599,100.031641,-5.792134,-8.174257 +100031933.567,100.031934,-6.126838,-8.137554 +100032226.536,100.032227,-6.056755,-8.091799 +100032519.504,100.032520,-5.928401,-8.047243 +100032812.473,100.032812,-5.617555,-8.013600 +100033105.441,100.033105,-5.321342,-7.980548 +100033398.410,100.033398,-6.068156,-7.948079 +100033691.378,100.033691,-5.228775,-7.921600 +100033984.347,100.033984,-4.534490,-7.890287 +100034277.315,100.034277,-5.053483,-7.858316 +100034570.284,100.034570,-5.139242,-7.825908 +100034863.252,100.034863,-5.093905,-7.795266 +100035156.221,100.035156,-5.480286,-7.763880 +100035449.189,100.035449,-4.967022,-7.732567 +100035742.158,100.035742,-5.560616,-7.703102 +100036035.126,100.036035,-6.092793,-7.674828 +100036328.095,100.036328,-5.519676,-7.645331 +100036621.063,100.036621,-5.940961,-7.612835 +100036914.032,100.036914,-6.755553,-7.581991 +100037207.000,100.037207,-6.546616,-7.551640 +100037499.969,100.037500,-5.932142,-7.517550 +100037792.937,100.037793,-6.405817,-7.480058 +100038085.906,100.038086,-6.725699,-7.442900 +100038378.874,100.038379,-6.144225,-7.408849 +100038671.843,100.038672,-5.991466,-7.374277 +100038964.811,100.038965,-6.279950,-7.340603 +100039257.780,100.039258,-7.189919,-7.305442 +100039550.748,100.039551,-6.433371,-7.273627 +100039843.717,100.039844,-5.922187,-7.242441 +100040136.685,100.040137,-6.364686,-7.213415 +100040429.654,100.040430,-6.401202,-7.186572 +100040722.622,100.040723,-5.846689,-7.160269 +100041015.591,100.041016,-6.553385,-7.124398 +100041308.559,100.041309,-7.023676,-7.081950 +100041601.528,100.041602,-7.339743,-7.052427 +100041894.496,100.041894,-6.709371,-7.029596 +100042187.465,100.042187,-6.638648,-7.005863 +100042480.433,100.042480,-6.238033,-6.980110 +100042773.402,100.042773,-5.743515,-6.951945 +100043066.370,100.043066,-6.743237,-6.923697 +100043359.339,100.043359,-6.504831,-6.897343 +100043652.307,100.043652,-6.017256,-6.869207 +100043945.276,100.043945,-6.496108,-6.841315 +100044238.244,100.044238,-6.395003,-6.818556 +100044531.213,100.044531,-6.566183,-6.798619 +100044824.181,100.044824,-5.853026,-6.775404 +100045117.150,100.045117,-6.463721,-6.748033 +100045410.118,100.045410,-6.974504,-6.718325 +100045703.087,100.045703,-7.452976,-6.692230 +100045996.055,100.045996,-6.545816,-6.672090 +100046289.024,100.046289,-5.899765,-6.645561 +100046581.992,100.046582,-6.300328,-6.620099 +100046874.961,100.046875,-7.037031,-6.596390 +100047167.929,100.047168,-6.853559,-6.573038 +100047460.898,100.047461,-7.081481,-6.548712 +100047753.866,100.047754,-6.653745,-6.520773 +100048046.835,100.048047,-6.465025,-6.494942 +100048339.803,100.048340,-6.366288,-6.470972 +100048632.772,100.048633,-6.344723,-6.447838 +100048925.740,100.048926,-6.148445,-6.433435 +100049218.709,100.049219,-6.425495,-6.415014 +100049511.677,100.049512,-6.053207,-6.397748 +100049804.646,100.049805,-6.703749,-6.384871 +100050097.615,100.050098,-7.566480,-6.373195 +100050390.583,100.050391,-7.796104,-6.368636 +100050683.552,100.050684,-6.726750,-6.361398 +100050976.520,100.050977,-6.727602,-6.354612 +100051269.489,100.051269,-6.692804,-6.355111 +100051562.457,100.051562,-7.656953,-6.354157 +100051855.426,100.051855,-7.370857,-6.347631 +100052148.394,100.052148,-6.370870,-6.341462 +100052441.363,100.052441,-6.399508,-6.335168 +100052734.331,100.052734,-7.522721,-6.321109 +100053027.300,100.053027,-7.549266,-6.306412 +100053320.268,100.053320,-7.608575,-6.300537 +100053613.237,100.053613,-8.366270,-6.291091 +100053906.205,100.053906,-7.625068,-6.289607 +100054199.174,100.054199,-7.518387,-6.295496 +100054492.142,100.054492,-7.456702,-6.293403 +100054785.111,100.054785,-7.631366,-6.293602 +100055078.079,100.055078,-7.419215,-6.290553 +100055371.048,100.055371,-7.370957,-6.282134 +100055664.016,100.055664,-7.673055,-6.276142 +100055956.985,100.055957,-7.768308,-6.264575 +100056249.953,100.056250,-7.550704,-6.246212 +100056542.922,100.056543,-7.108349,-6.229827 +100056835.890,100.056836,-7.245820,-6.207713 +100057128.859,100.057129,-7.243485,-6.174797 +100057421.827,100.057422,-6.801713,-6.143033 +100057714.796,100.057715,-6.450202,-6.123760 +100058007.764,100.058008,-6.457731,-6.097743 +100058300.733,100.058301,-6.721420,-6.065286 +100058593.701,100.058594,-6.702753,-6.031050 +100058886.670,100.058887,-6.684473,-6.002431 +100059179.638,100.059180,-6.460963,-5.964526 +100059472.607,100.059473,-6.791273,-5.923227 +100059765.575,100.059766,-6.903805,-5.886185 +100060058.544,100.060059,-7.109853,-5.853853 +100060351.512,100.060352,-7.298793,-5.815243 +100060644.481,100.060644,-7.273486,-5.771468 +100060937.449,100.060937,-6.037193,-5.735868 +100061230.418,100.061230,-5.293457,-5.695195 +100061523.386,100.061523,-6.607862,-5.647328 +100061816.355,100.061816,-7.480077,-5.599540 +100062109.323,100.062109,-7.168192,-5.558880 +100062402.292,100.062402,-6.858617,-5.518330 +100062695.260,100.062695,-6.679880,-5.472591 +100062988.229,100.062988,-6.654565,-5.428899 +100063281.197,100.063281,-6.702173,-5.383927 +100063574.166,100.063574,-6.488434,-5.344246 +100063867.134,100.063867,-6.252152,-5.307521 +100064160.103,100.064160,-6.765977,-5.265343 +100064453.071,100.064453,-7.174008,-5.229666 +100064746.040,100.064746,-6.770008,-5.187671 +100065039.008,100.065039,-6.169874,-5.144934 +100065331.977,100.065332,-5.770638,-5.102131 +100065624.945,100.065625,-6.001169,-5.055312 +100065917.914,100.065918,-6.433544,-5.008407 +100066210.882,100.066211,-5.836475,-4.957712 +100066503.851,100.066504,-5.983089,-4.914304 +100066796.819,100.066797,-5.714108,-4.867066 +100067089.788,100.067090,-5.712459,-4.811125 +100067382.756,100.067383,-5.632020,-4.762027 +100067675.725,100.067676,-5.416462,-4.712446 +100067968.693,100.067969,-5.837634,-4.655954 +100068261.662,100.068262,-5.713389,-4.601254 +100068554.630,100.068555,-5.385035,-4.550808 +100068847.599,100.068848,-5.480496,-4.503920 +100069140.567,100.069141,-5.076570,-4.459626 +100069433.536,100.069434,-5.001951,-4.414364 +100069726.504,100.069727,-5.400597,-4.373036 +100070019.473,100.070019,-5.555944,-4.329015 +100070312.441,100.070312,-6.228271,-4.277729 +100070605.410,100.070605,-5.672035,-4.228037 +100070898.378,100.070898,-5.221176,-4.183679 +100071191.347,100.071191,-5.456631,-4.138046 +100071484.315,100.071484,-5.050890,-4.092825 +100071777.284,100.071777,-5.069588,-4.036115 +100072070.252,100.072070,-5.389353,-3.981870 +100072363.221,100.072363,-5.315641,-3.939930 +100072656.189,100.072656,-4.453160,-3.900507 +100072949.158,100.072949,-4.171957,-3.853680 +100073242.126,100.073242,-4.722604,-3.808390 +100073535.095,100.073535,-4.986665,-3.756525 +100073828.063,100.073828,-5.072408,-3.695624 +100074121.032,100.074121,-5.109131,-3.637235 +100074414.000,100.074414,-4.843147,-3.575833 +100074706.969,100.074707,-5.160190,-3.516245 +100074999.938,100.075000,-4.787992,-3.461720 +100075292.906,100.075293,-4.614586,-3.398567 +100075585.875,100.075586,-4.399001,-3.327549 +100075878.843,100.075879,-4.388268,-3.266461 +100076171.812,100.076172,-4.167170,-3.208025 +100076464.780,100.076465,-3.941860,-3.141168 +100076757.749,100.076758,-3.754630,-3.059165 +100077050.717,100.077051,-3.366171,-2.978863 +100077343.686,100.077344,-3.333570,-2.905730 +100077636.654,100.077637,-3.944393,-2.816805 +100077929.623,100.077930,-3.667915,-2.730354 +100078222.591,100.078223,-3.326007,-2.657495 +100078515.560,100.078516,-2.913466,-2.570849 +100078808.528,100.078809,-3.209531,-2.471679 +100079101.497,100.079101,-2.700225,-2.398479 +100079394.465,100.079394,-2.819157,-2.335018 +100079687.434,100.079687,-2.778932,-2.273569 +100079980.402,100.079980,-2.842393,-2.213594 +100080273.371,100.080273,-2.606030,-2.151240 +100080566.339,100.080566,-2.270971,-2.088073 +100080859.308,100.080859,-2.527687,-2.030963 +100081152.276,100.081152,-2.477682,-1.978730 +100081445.245,100.081445,-2.164117,-1.924446 +100081738.213,100.081738,-2.236783,-1.865872 +100082031.182,100.082031,-2.454183,-1.809814 +100082324.150,100.082324,-2.409609,-1.764392 +100082617.119,100.082617,-1.905280,-1.719732 +100082910.087,100.082910,-1.808828,-1.675087 +100083203.056,100.083203,-2.074486,-1.635921 +100083496.024,100.083496,-2.309001,-1.596441 +100083788.993,100.083789,-2.291023,-1.552556 +100084081.961,100.084082,-2.090331,-1.506596 +100084374.930,100.084375,-2.475417,-1.465254 +100084667.898,100.084668,-2.077775,-1.430818 +100084960.867,100.084961,-1.738309,-1.394285 +100085253.835,100.085254,-1.933047,-1.356749 +100085546.804,100.085547,-1.808759,-1.322171 +100085839.772,100.085840,-1.909487,-1.289645 +100086132.741,100.086133,-1.371483,-1.260213 +100086425.709,100.086426,-1.570749,-1.233816 +100086718.678,100.086719,-1.440945,-1.207642 +100087011.646,100.087012,-1.096114,-1.180837 +100087304.615,100.087305,-1.421720,-1.155445 +100087597.583,100.087598,-1.419063,-1.130433 +100087890.552,100.087891,-0.850558,-1.106613 +100088183.520,100.088184,-0.861521,-1.085613 +100088476.489,100.088476,-1.047428,-1.066847 +100088769.457,100.088769,-1.228966,-1.045654 +100089062.426,100.089062,-1.304098,-1.024867 +100089355.394,100.089355,-1.297391,-1.004713 +100089648.363,100.089648,-1.413667,-0.988647 +100089941.331,100.089941,-1.405475,-0.971138 +100090234.300,100.090234,-1.102777,-0.954100 +100090527.268,100.090527,-1.221297,-0.932520 +100090820.237,100.090820,-1.285622,-0.913666 +100091113.205,100.091113,-1.165190,-0.896258 +100091406.174,100.091406,-1.151111,-0.877758 +100091699.142,100.091699,-0.588806,-0.860895 +100091992.111,100.091992,-0.639744,-0.844122 +100092285.079,100.092285,-1.179929,-0.827118 +100092578.048,100.092578,-1.346653,-0.811421 +100092871.016,100.092871,-1.068765,-0.800006 +100093163.985,100.093164,-1.151109,-0.792262 +100093456.953,100.093457,-0.663865,-0.782854 +100093749.922,100.093750,-0.176002,-0.771866 +100094042.890,100.094043,-0.156477,-0.759166 +100094335.859,100.094336,0.091630,-0.746245 +100094628.827,100.094629,0.044939,-0.734661 +100094921.796,100.094922,-0.255846,-0.724680 +100095214.764,100.095215,0.356213,-0.713949 +100095507.733,100.095508,0.849183,-0.703624 +100095800.701,100.095801,0.305454,-0.694402 +100096093.670,100.096094,0.178474,-0.687017 +100096386.638,100.096387,0.759595,-0.678751 +100096679.607,100.096680,1.648135,-0.670490 +100096972.575,100.096973,1.621481,-0.665688 +100097265.544,100.097266,1.336597,-0.662456 +100097558.512,100.097559,2.202187,-0.657107 +100097851.481,100.097851,2.217861,-0.652433 +100098144.449,100.098144,1.649808,-0.650506 +100098437.418,100.098437,2.329768,-0.648955 +100098730.386,100.098730,2.932081,-0.648479 +100099023.355,100.099023,1.854435,-0.643223 +100099316.323,100.099316,1.419963,-0.640870 +100099609.292,100.099609,1.302263,-0.641404 +100099902.260,100.099902,1.247046,-0.643012 +100100195.229,100.100195,1.413244,-0.643736 +100100488.198,100.100488,1.490239,-0.646454 +100100781.166,100.100781,1.168049,-0.651943 +100101074.135,100.101074,1.097560,-0.656166 +100101367.103,100.101367,1.424301,-0.659101 +100101660.072,100.101660,1.500564,-0.664610 +100101953.040,100.101953,1.260897,-0.670904 +100102246.009,100.102246,0.574546,-0.675717 +100102538.977,100.102539,0.604820,-0.682420 +100102831.946,100.102832,0.672682,-0.690868 +100103124.914,100.103125,0.252853,-0.700514 +100103417.883,100.103418,0.302366,-0.707690 +100103710.851,100.103711,0.747714,-0.714276 +100104003.820,100.104004,0.991431,-0.721387 +100104296.788,100.104297,0.557029,-0.728624 +100104589.757,100.104590,-0.132338,-0.735445 +100104882.725,100.104883,0.182149,-0.744867 +100105175.694,100.105176,0.430528,-0.754765 +100105468.662,100.105469,0.278005,-0.762183 +100105761.631,100.105762,0.046238,-0.771472 +100106054.599,100.106055,-0.364150,-0.780650 +100106347.568,100.106348,-0.527120,-0.794940 +100106640.536,100.106641,-0.577377,-0.809399 +100106933.505,100.106934,-0.406842,-0.824450 +100107226.473,100.107226,-0.552763,-0.840836 +100107519.442,100.107519,-0.556023,-0.855178 +100107812.410,100.107812,-0.614215,-0.869324 +100108105.379,100.108105,-1.104435,-0.887762 +100108398.347,100.108398,-1.364618,-0.906767 +100108691.316,100.108691,-0.895782,-0.925785 +100108984.284,100.108984,-0.970381,-0.942951 +100109277.253,100.109277,-0.893828,-0.960079 +100109570.221,100.109570,-1.432918,-0.977433 +100109863.190,100.109863,-1.371989,-0.993573 +100110156.158,100.110156,-1.494481,-1.008795 +100110449.127,100.110449,-1.044804,-1.025300 +100110742.095,100.110742,-1.239194,-1.041658 +100111035.064,100.111035,-1.270420,-1.058003 +100111328.032,100.111328,-1.189234,-1.076708 +100111621.001,100.111621,-1.261078,-1.095888 +100111913.969,100.111914,-1.272578,-1.120119 +100112206.938,100.112207,-1.357083,-1.144180 +100112499.906,100.112500,-1.523725,-1.164832 +100112792.875,100.112793,-1.819101,-1.184006 +100113085.843,100.113086,-2.320799,-1.205950 +100113378.812,100.113379,-2.331223,-1.227557 +100113671.780,100.113672,-2.167911,-1.253252 +100113964.749,100.113965,-1.891800,-1.283245 +100114257.717,100.114258,-1.863370,-1.313825 +100114550.686,100.114551,-1.965502,-1.348162 +100114843.654,100.114844,-2.434154,-1.382337 +100115136.623,100.115137,-2.083805,-1.413379 +100115429.591,100.115430,-2.060483,-1.450725 +100115722.560,100.115723,-2.140792,-1.494724 +100116015.528,100.116016,-2.496812,-1.532282 +100116308.497,100.116308,-2.176023,-1.568063 +100116601.465,100.116601,-2.030310,-1.610824 +100116894.434,100.116894,-2.587309,-1.667023 +100117187.402,100.117187,-2.611031,-1.725103 +100117480.371,100.117480,-2.153112,-1.780076 +100117773.339,100.117773,-2.756911,-1.851101 +100118066.308,100.118066,-3.168871,-1.923110 +100118359.276,100.118359,-2.949303,-1.985432 +100118652.245,100.118652,-2.805152,-2.060961 +100118945.213,100.118945,-2.072662,-2.152073 +100119238.182,100.119238,-2.211039,-2.221135 +100119531.150,100.119531,-2.941623,-2.283647 +100119824.119,100.119824,-3.155217,-2.344833 +100120117.087,100.120117,-3.010250,-2.404541 +100120410.056,100.120410,-3.234139,-2.469522 +100120703.024,100.120703,-3.529230,-2.538164 +100120995.993,100.120996,-3.524364,-2.602197 +100121288.961,100.121289,-3.136775,-2.666807 +100121581.930,100.121582,-3.388004,-2.737730 +100121874.898,100.121875,-3.693490,-2.810948 +100122167.867,100.122168,-3.582554,-2.882030 +100122460.835,100.122461,-4.052467,-2.943261 +100122753.804,100.122754,-3.794969,-3.004719 +100123046.772,100.123047,-3.978162,-3.068614 +100123339.741,100.123340,-3.688408,-3.127381 +100123632.709,100.123633,-3.858951,-3.187435 +100123925.678,100.123926,-3.980215,-3.254555 +100124218.646,100.124219,-3.718502,-3.327835 +100124511.615,100.124512,-4.159544,-3.393569 +100124804.583,100.124805,-4.328525,-3.447205 +100125097.552,100.125098,-3.899543,-3.508312 +100125390.521,100.125391,-3.523776,-3.576141 +100125683.489,100.125683,-3.832324,-3.642815 +100125976.458,100.125976,-3.956690,-3.704369 +100126269.426,100.126269,-4.519835,-3.761145 +100126562.395,100.126562,-4.984933,-3.814871 +100126855.363,100.126855,-4.891098,-3.870437 +100127148.332,100.127148,-4.556727,-3.927099 +100127441.300,100.127441,-4.575843,-3.983893 +100127734.269,100.127734,-4.493254,-4.042210 +100128027.237,100.128027,-4.596363,-4.100419 +100128320.206,100.128320,-4.782897,-4.149456 +100128613.174,100.128613,-5.230410,-4.193998 +100128906.143,100.128906,-4.991536,-4.248331 +100129199.111,100.129199,-5.133894,-4.300860 +100129492.080,100.129492,-5.174746,-4.356385 +100129785.048,100.129785,-4.997675,-4.404873 +100130078.017,100.130078,-4.657263,-4.454164 +100130370.985,100.130371,-4.391330,-4.503141 +100130663.954,100.130664,-4.582889,-4.558826 +100130956.922,100.130957,-4.699470,-4.610908 +100131249.891,100.131250,-5.182773,-4.661888 +100131542.859,100.131543,-5.286929,-4.714616 +100131835.828,100.131836,-5.513477,-4.766732 +100132128.796,100.132129,-5.556530,-4.818556 +100132421.765,100.132422,-5.805544,-4.869296 +100132714.733,100.132715,-5.656826,-4.921452 +100133007.702,100.133008,-5.915126,-4.968215 +100133300.670,100.133301,-5.984187,-5.008064 +100133593.639,100.133594,-6.036879,-5.050310 +100133886.607,100.133887,-5.955698,-5.094448 +100134179.576,100.134180,-6.033737,-5.144115 +100134472.544,100.134473,-6.496297,-5.198107 +100134765.513,100.134766,-6.514184,-5.249373 +100135058.481,100.135058,-6.325716,-5.291669 +100135351.450,100.135351,-6.228575,-5.341000 +100135644.418,100.135644,-6.436675,-5.392372 +100135937.387,100.135937,-6.258079,-5.437687 +100136230.355,100.136230,-5.985269,-5.478534 +100136523.324,100.136523,-5.923727,-5.524297 +100136816.292,100.136816,-6.090595,-5.572303 +100137109.261,100.137109,-6.720945,-5.616850 +100137402.229,100.137402,-6.945179,-5.664018 +100137695.198,100.137695,-7.318189,-5.717738 +100137988.166,100.137988,-7.090968,-5.758958 +100138281.135,100.138281,-6.808325,-5.794679 +100138574.103,100.138574,-6.683454,-5.830357 +100138867.072,100.138867,-7.105651,-5.870778 +100139160.040,100.139160,-6.658428,-5.924682 +100139453.009,100.139453,-6.721417,-5.976225 +100139745.977,100.139746,-6.638868,-6.016045 +100140038.946,100.140039,-6.154265,-6.055487 +100140331.914,100.140332,-6.674882,-6.093498 +100140624.883,100.140625,-7.149743,-6.133747 +100140917.851,100.140918,-7.191885,-6.169234 +100141210.820,100.141211,-7.536825,-6.203775 +100141503.788,100.141504,-7.275953,-6.245518 +100141796.757,100.141797,-7.135765,-6.278391 +100142089.725,100.142090,-7.629264,-6.306313 +100142382.694,100.142383,-8.133607,-6.340558 +100142675.662,100.142676,-7.646516,-6.368922 +100142968.631,100.142969,-7.803808,-6.397642 +100143261.599,100.143262,-8.193867,-6.419109 +100143554.568,100.143555,-8.056919,-6.446408 +100143847.536,100.143848,-7.463253,-6.478021 +100144140.505,100.144141,-7.619802,-6.510400 +100144433.473,100.144433,-7.437453,-6.542323 +100144726.442,100.144726,-6.984994,-6.570798 +100145019.410,100.145019,-7.511374,-6.591196 +100145312.379,100.145312,-8.021924,-6.615963 +100145605.347,100.145605,-8.239505,-6.642563 +100145898.316,100.145898,-7.511976,-6.665775 +100146191.284,100.146191,-7.923690,-6.690990 +100146484.253,100.146484,-7.514855,-6.709232 +100146777.221,100.146777,-8.183151,-6.725445 +100147070.190,100.147070,-7.394647,-6.744569 +100147363.158,100.147363,-7.961676,-6.760334 +100147656.127,100.147656,-8.249279,-6.779177 +100147949.095,100.147949,-8.267976,-6.800846 +100148242.064,100.148242,-7.518118,-6.815879 +100148535.032,100.148535,-7.151940,-6.823902 +100148828.001,100.148828,-7.820946,-6.830326 +100149120.969,100.149121,-7.435428,-6.841197 +100149413.938,100.149414,-7.759970,-6.841504 +100149706.906,100.149707,-7.928498,-6.841500 +100149999.875,100.150000,-7.736868,-6.856141 +100150292.844,100.150293,-8.014452,-6.881291 +100150585.812,100.150586,-7.764355,-6.906803 +100150878.781,100.150879,-7.444634,-6.931304 +100151171.749,100.151172,-7.135197,-6.957848 +100151464.718,100.151465,-7.092638,-6.982848 +100151757.686,100.151758,-7.074992,-7.008639 +100152050.655,100.152051,-6.909195,-7.035964 +100152343.623,100.152344,-6.841319,-7.067260 +100152636.592,100.152637,-7.667048,-7.093229 +100152929.560,100.152930,-7.320358,-7.120243 +100153222.529,100.153223,-7.271697,-7.144938 +100153515.497,100.153515,-7.798441,-7.172735 +100153808.466,100.153808,-7.594365,-7.198566 +100154101.434,100.154101,-7.829274,-7.228792 +100154394.403,100.154394,-8.750723,-7.261273 +100154687.371,100.154687,-8.231570,-7.288883 +100154980.340,100.154980,-7.582413,-7.317175 +100155273.308,100.155273,-7.921967,-7.347147 +100155566.277,100.155566,-8.202863,-7.379985 +100155859.245,100.155859,-6.990029,-7.411379 +100156152.214,100.156152,-7.075904,-7.445481 +100156445.182,100.156445,-7.007932,-7.483039 +100156738.151,100.156738,-6.893417,-7.521291 +100157031.119,100.157031,-7.861595,-7.558819 +100157324.088,100.157324,-8.473044,-7.591610 +100157617.056,100.157617,-8.180695,-7.623035 +100157910.025,100.157910,-7.372454,-7.651638 +100158202.993,100.158203,-7.349369,-7.682390 +100158495.962,100.158496,-6.738836,-7.716214 +100158788.930,100.158789,-7.151669,-7.751185 +100159081.899,100.159082,-7.371100,-7.782599 +100159374.867,100.159375,-7.260236,-7.819176 +100159667.836,100.159668,-7.163437,-7.856116 +100159960.804,100.159961,-7.603929,-7.893642 +100160253.773,100.160254,-6.905138,-7.937051 +100160546.741,100.160547,-7.846526,-7.976037 +100160839.710,100.160840,-7.641312,-8.011149 +100161132.678,100.161133,-7.399617,-8.045777 +100161425.647,100.161426,-7.655939,-8.077305 +100161718.615,100.161719,-6.713783,-8.112012 +100162011.584,100.162012,-6.568367,-8.148706 +100162304.552,100.162305,-7.295712,-8.181520 +100162597.521,100.162598,-7.300121,-8.210737 +100162890.489,100.162890,-6.798545,-8.244079 +100163183.458,100.163183,-6.103400,-8.276687 +100163476.426,100.163476,-6.346487,-8.306638 +100163769.395,100.163769,-7.303974,-8.337620 +100164062.363,100.164062,-7.671099,-8.373781 +100164355.332,100.164355,-6.974829,-8.409187 +100164648.300,100.164648,-7.322335,-8.446160 +100164941.269,100.164941,-6.426239,-8.487834 +100165234.237,100.165234,-6.259117,-8.525333 +100165527.206,100.165527,-5.818602,-8.559088 +100165820.174,100.165820,-5.924928,-8.591296 +100166113.143,100.166113,-6.360302,-8.629633 +100166406.111,100.166406,-6.379491,-8.665042 +100166699.080,100.166699,-6.810766,-8.704870 +100166992.048,100.166992,-7.070341,-8.739135 +100167285.017,100.167285,-6.105580,-8.780774 +100167577.985,100.167578,-6.504910,-8.817303 +100167870.954,100.167871,-6.732182,-8.851922 +100168163.922,100.168164,-6.053088,-8.886695 +100168456.891,100.168457,-5.531507,-8.928690 +100168749.859,100.168750,-5.886958,-8.975080 +100169042.828,100.169043,-6.094378,-9.015134 +100169335.796,100.169336,-5.162406,-9.059376 +100169628.765,100.169629,-5.174357,-9.100631 +100169921.733,100.169922,-6.556453,-9.140750 +100170214.702,100.170215,-7.470608,-9.183396 +100170507.670,100.170508,-7.005687,-9.223488 +100170800.639,100.170801,-7.189888,-9.266487 +100171093.607,100.171094,-7.704035,-9.313580 +100171386.576,100.171387,-8.393383,-9.364856 +100171679.544,100.171680,-8.744547,-9.417201 +100171972.513,100.171973,-9.591757,-9.470541 +100172265.481,100.172265,-10.781033,-9.526928 +100172558.450,100.172558,-9.904140,-9.584747 +100172851.418,100.172851,-9.749575,-9.632959 +100173144.387,100.173144,-9.771990,-9.686034 +100173437.355,100.173437,-10.786927,-9.740422 +100173730.324,100.173730,-10.301914,-9.789188 +100174023.292,100.174023,-11.387542,-9.841105 +100174316.261,100.174316,-12.516633,-9.890799 +100174609.229,100.174609,-12.150299,-9.931103 +100174902.198,100.174902,-12.436240,-9.977235 +100175195.167,100.175195,-12.344599,-10.031543 +100175488.135,100.175488,-13.167519,-10.082266 +100175781.104,100.175781,-13.300421,-10.130351 +100176074.072,100.176074,-13.745354,-10.195170 +100176367.041,100.176367,-14.021549,-10.259647 +100176660.009,100.176660,-13.854654,-10.326163 +100176952.978,100.176953,-14.269146,-10.395581 +100177245.946,100.177246,-14.951575,-10.451786 +100177538.915,100.177539,-15.217690,-10.501025 +100177831.883,100.177832,-15.232255,-10.554410 +100178124.852,100.178125,-15.318392,-10.619870 +100178417.820,100.178418,-15.638783,-10.686744 +100178710.789,100.178711,-15.418591,-10.765132 +100179003.757,100.179004,-15.374223,-10.837643 +100179296.726,100.179297,-16.125500,-10.907634 +100179589.694,100.179590,-16.773228,-10.980567 +100179882.663,100.179883,-16.247990,-11.056530 +100180175.631,100.180176,-16.751955,-11.126331 +100180468.600,100.180469,-17.409683,-11.209825 +100180761.568,100.180762,-17.706167,-11.278092 +100181054.537,100.181055,-17.233751,-11.350906 +100181347.505,100.181348,-16.795137,-11.429362 +100181640.474,100.181640,-17.579264,-11.504618 +100181933.442,100.181933,-18.137814,-11.600077 +100182226.411,100.182226,-18.332952,-11.701125 +100182519.379,100.182519,-18.519491,-11.788242 +100182812.348,100.182812,-18.622211,-11.877119 +100183105.316,100.183105,-19.110865,-11.979346 +100183398.285,100.183398,-19.372406,-12.102747 +100183691.253,100.183691,-18.995922,-12.222720 +100183984.222,100.183984,-18.939829,-12.321032 +100184277.190,100.184277,-19.496384,-12.413084 +100184570.159,100.184570,-18.896679,-12.524012 +100184863.127,100.184863,-18.825438,-12.629117 +100185156.096,100.185156,-19.778463,-12.762212 +100185449.064,100.185449,-20.586535,-12.904986 +100185742.033,100.185742,-20.490561,-13.068792 +100186035.001,100.186035,-19.665665,-13.234670 +100186327.970,100.186328,-20.562235,-13.390441 +100186620.938,100.186621,-21.458502,-13.551171 +100186913.907,100.186914,-21.628225,-13.701894 +100187206.875,100.187207,-21.289732,-13.848908 +100187499.844,100.187500,-20.993099,-14.040222 +100187792.812,100.187793,-21.658001,-14.222273 +100188085.781,100.188086,-21.744785,-14.402353 +100188378.749,100.188379,-21.721453,-14.623059 +100188671.718,100.188672,-21.948565,-14.886265 +100188964.686,100.188965,-22.283653,-15.143710 +100189257.655,100.189258,-22.009716,-15.404150 +100189550.623,100.189551,-21.775175,-15.750902 +100189843.592,100.189844,-22.103468,-16.126676 +100190136.560,100.190137,-22.814142,-16.421305 +100190429.529,100.190430,-22.197588,-16.675081 +100190722.497,100.190722,-22.267824,-16.976823 +100191015.466,100.191015,-22.867128,-17.286801 +100191308.434,100.191308,-22.946819,-17.581725 +100191601.403,100.191601,-22.727875,-17.849948 +100191894.371,100.191894,-23.134129,-18.112564 +100192187.340,100.192187,-23.871828,-18.340504 +100192480.308,100.192480,-23.080843,-18.521139 +100192773.277,100.192773,-23.283430,-18.753525 +100193066.245,100.193066,-23.710339,-19.008712 +100193359.214,100.193359,-23.826872,-19.278158 +100193652.182,100.193652,-24.411573,-19.502634 +100193945.151,100.193945,-25.071548,-19.769218 +100194238.119,100.194238,-24.994450,-19.987221 +100194531.088,100.194531,-24.491053,-20.161223 +100194824.056,100.194824,-24.963400,-20.360522 +100195117.025,100.195117,-24.759378,-20.556132 +100195409.993,100.195410,-24.640934,-20.765727 +100195702.962,100.195703,-24.896851,-20.945443 +100195995.930,100.195996,-25.184935,-21.126599 +100196288.899,100.196289,-25.159597,-21.296109 +100196581.867,100.196582,-25.205118,-21.460137 +100196874.836,100.196875,-25.284025,-21.638244 +100197167.804,100.197168,-25.529205,-21.806577 +100197460.773,100.197461,-25.313465,-21.955561 +100197753.741,100.197754,-25.386133,-22.099823 +100198046.710,100.198047,-25.617935,-22.247996 +100198339.678,100.198340,-25.838213,-22.397730 +100198632.647,100.198633,-25.728361,-22.541461 +100198925.615,100.198926,-26.054729,-22.699422 +100199218.584,100.199219,-25.875864,-22.864377 +100199511.552,100.199512,-25.526184,-23.005967 +100199804.521,100.199805,-25.812674,-23.130233 +100200097.490,100.200097,-26.270201,-23.275778 +100200390.458,100.200390,-25.756437,-23.409233 +100200683.427,100.200683,-26.064598,-23.526476 +100200976.395,100.200976,-26.151363,-23.637164 +100201269.364,100.201269,-26.418306,-23.764232 +100201562.332,100.201562,-26.563002,-23.911704 +100201855.301,100.201855,-26.045824,-24.037795 +100202148.269,100.202148,-26.199736,-24.151260 +100202441.238,100.202441,-26.032400,-24.260832 +100202734.206,100.202734,-26.284864,-24.368058 +100203027.175,100.203027,-26.707537,-24.475346 +100203320.143,100.203320,-27.062117,-24.568965 +100203613.112,100.203613,-27.191257,-24.658090 +100203906.080,100.203906,-26.517412,-24.761617 +100204199.049,100.204199,-25.874113,-24.869796 +100204492.017,100.204492,-26.070314,-24.965384 +100204784.986,100.204785,-26.863483,-25.079890 +100205077.954,100.205078,-26.993694,-25.199104 +100205370.923,100.205371,-26.808424,-25.294481 +100205663.891,100.205664,-26.730257,-25.372177 +100205956.860,100.205957,-26.378019,-25.453280 +100206249.828,100.206250,-26.985273,-25.557078 +100206542.797,100.206543,-26.677584,-25.641232 +100206835.765,100.206836,-26.736601,-25.706400 +100207128.734,100.207129,-27.246363,-25.767609 +100207421.702,100.207422,-27.475605,-25.839064 +100207714.671,100.207715,-27.493601,-25.917302 +100208007.639,100.208008,-27.588827,-25.982570 +100208300.608,100.208301,-27.442354,-26.049819 +100208593.576,100.208594,-27.130243,-26.116745 +100208886.545,100.208887,-27.339251,-26.179675 +100209179.513,100.209180,-27.374643,-26.236627 +100209472.482,100.209472,-27.677662,-26.299041 +100209765.450,100.209765,-27.680716,-26.367724 +100210058.419,100.210058,-27.563868,-26.432608 +100210351.387,100.210351,-27.524580,-26.485244 +100210644.356,100.210644,-28.127865,-26.551645 +100210937.324,100.210937,-27.751276,-26.616703 +100211230.293,100.211230,-27.775671,-26.672369 +100211523.261,100.211523,-28.033699,-26.725075 +100211816.230,100.211816,-27.867208,-26.781446 +100212109.198,100.212109,-27.761648,-26.833213 +100212402.167,100.212402,-27.562698,-26.876147 +100212695.135,100.212695,-27.226042,-26.932921 +100212988.104,100.212988,-27.532139,-26.986161 +100213281.072,100.213281,-27.342140,-27.030828 +100213574.041,100.213574,-27.132401,-27.074120 +100213867.009,100.213867,-27.396521,-27.109665 +100214159.978,100.214160,-27.170244,-27.137000 +100214452.946,100.214453,-26.869501,-27.166246 +100214745.915,100.214746,-27.595278,-27.204011 +100215038.883,100.215039,-28.137621,-27.234076 +100215331.852,100.215332,-28.257853,-27.265831 +100215624.820,100.215625,-27.787933,-27.299379 +100215917.789,100.215918,-27.575953,-27.330826 +100216210.757,100.216211,-27.557047,-27.356923 +100216503.726,100.216504,-27.127594,-27.384479 +100216796.694,100.216797,-27.363503,-27.410449 +100217089.663,100.217090,-27.695074,-27.436276 +100217382.631,100.217383,-28.241690,-27.459347 +100217675.600,100.217676,-28.112883,-27.486768 +100217968.568,100.217969,-27.851042,-27.513860 +100218261.537,100.218262,-27.648523,-27.541608 +100218554.505,100.218555,-27.903791,-27.561928 +100218847.474,100.218847,-28.353830,-27.582049 +100219140.442,100.219140,-27.993155,-27.601001 +100219433.411,100.219433,-27.746405,-27.623329 +100219726.379,100.219726,-27.745466,-27.651112 +100220019.348,100.220019,-27.536093,-27.673451 +100220312.316,100.220312,-27.890844,-27.692046 +100220605.285,100.220605,-28.202314,-27.715483 +100220898.253,100.220898,-27.819483,-27.734355 +100221191.222,100.221191,-27.422556,-27.752634 +100221484.190,100.221484,-27.718271,-27.769935 +100221777.159,100.221777,-28.143818,-27.783570 +100222070.127,100.222070,-28.505329,-27.804078 +100222363.096,100.222363,-28.079834,-27.820640 +100222656.064,100.222656,-28.135063,-27.838054 +100222949.033,100.222949,-28.221039,-27.857764 +100223242.001,100.223242,-27.336416,-27.874557 +100223534.970,100.223535,-27.360401,-27.888476 +100223827.938,100.223828,-28.069025,-27.901203 +100224120.907,100.224121,-28.223438,-27.915945 +100224413.875,100.224414,-28.206797,-27.937362 +100224706.844,100.224707,-28.156343,-27.960148 +100224999.812,100.225000,-27.989050,-27.975905 +100225292.781,100.225293,-28.140697,-27.989507 +100225585.750,100.225586,-28.072720,-28.003031 +100225878.718,100.225879,-28.027672,-28.017068 +100226171.687,100.226172,-28.006029,-28.037554 +100226464.655,100.226465,-28.319220,-28.052918 +100226757.624,100.226758,-28.011412,-28.068467 +100227050.592,100.227051,-27.643225,-28.080979 +100227343.561,100.227344,-28.220861,-28.089647 +100227636.529,100.227637,-28.013315,-28.097329 +100227929.498,100.227929,-27.973127,-28.105903 +100228222.466,100.228222,-28.593260,-28.112707 +100228515.435,100.228515,-28.167006,-28.122953 +100228808.403,100.228808,-28.053408,-28.134387 +100229101.372,100.229101,-27.934301,-28.143122 +100229394.340,100.229394,-27.836866,-28.154441 +100229687.309,100.229687,-27.946434,-28.164274 +100229980.277,100.229980,-28.320057,-28.173840 +100230273.246,100.230273,-28.302034,-28.185637 +100230566.214,100.230566,-28.666317,-28.197410 +100230859.183,100.230859,-28.459242,-28.201821 +100231152.151,100.231152,-28.648527,-28.207393 +100231445.120,100.231445,-28.187542,-28.213515 +100231738.088,100.231738,-28.012362,-28.216879 +100232031.057,100.232031,-28.376245,-28.223468 +100232324.025,100.232324,-28.840546,-28.228345 +100232616.994,100.232617,-28.973757,-28.234545 +100232909.962,100.232910,-28.817812,-28.242541 +100233202.931,100.233203,-28.387089,-28.247186 +100233495.899,100.233496,-28.383856,-28.259239 +100233788.868,100.233789,-28.444418,-28.271570 +100234081.836,100.234082,-28.421587,-28.281086 +100234374.805,100.234375,-28.573942,-28.292356 +100234667.773,100.234668,-28.935820,-28.307228 +100234960.742,100.234961,-28.570541,-28.311838 +100235253.710,100.235254,-28.369894,-28.310111 +100235546.679,100.235547,-28.367466,-28.310599 +100235839.647,100.235840,-28.546574,-28.317165 +100236132.616,100.236133,-28.201948,-28.322855 +100236425.584,100.236426,-28.369316,-28.330632 +100236718.553,100.236719,-28.168020,-28.343749 +100237011.521,100.237012,-28.275381,-28.355249 +100237304.490,100.237304,-28.269180,-28.361092 +100237597.458,100.237597,-28.556541,-28.362597 +100237890.427,100.237890,-28.621538,-28.362257 +100238183.395,100.238183,-29.221834,-28.364253 +100238476.364,100.238476,-28.294950,-28.373445 +100238769.332,100.238769,-28.058050,-28.378619 +100239062.301,100.239062,-28.418007,-28.379973 +100239355.269,100.239355,-28.651375,-28.384233 +100239648.238,100.239648,-28.889368,-28.388261 +100239941.206,100.239941,-28.491205,-28.396428 +100240234.175,100.240234,-28.663887,-28.406801 +100240527.143,100.240527,-28.531536,-28.415677 +100240820.112,100.240820,-28.328058,-28.420981 +100241113.080,100.241113,-28.367311,-28.429119 +100241406.049,100.241406,-28.641108,-28.438255 +100241699.017,100.241699,-28.271564,-28.444882 +100241991.986,100.241992,-28.506702,-28.446784 +100242284.954,100.242285,-28.140659,-28.449143 +100242577.923,100.242578,-27.992441,-28.453210 +100242870.891,100.242871,-28.755409,-28.456436 +100243163.860,100.243164,-28.967352,-28.460558 +100243456.828,100.243457,-29.024672,-28.473207 +100243749.797,100.243750,-29.005331,-28.484362 +100244042.765,100.244043,-28.306007,-28.488199 +100244335.734,100.244336,-28.248512,-28.490455 +100244628.702,100.244629,-28.798292,-28.495278 +100244921.671,100.244922,-28.977762,-28.499265 +100245214.639,100.245215,-28.809406,-28.504251 +100245507.608,100.245508,-28.512377,-28.511629 +100245800.576,100.245801,-28.468184,-28.516619 +100246093.545,100.246094,-28.911970,-28.519040 +100246386.513,100.246387,-29.059299,-28.523267 +100246679.482,100.246679,-28.593546,-28.526511 +100246972.450,100.246972,-28.224232,-28.531283 +100247265.419,100.247265,-28.355543,-28.537257 +100247558.387,100.247558,-28.500444,-28.536916 +100247851.356,100.247851,-28.657824,-28.537991 +100248144.324,100.248144,-28.506052,-28.541180 +100248437.293,100.248437,-28.848841,-28.540823 +100248730.261,100.248730,-28.597111,-28.544494 +100249023.230,100.249023,-28.469936,-28.548789 +100249316.198,100.249316,-28.915876,-28.555121 +100249609.167,100.249609,-29.090130,-28.556086 +100249902.135,100.249902,-29.045506,-28.558373 +100250195.104,100.250195,-29.257603,-28.561675 +100250488.073,100.250488,-29.190046,-28.565939 +100250781.041,100.250781,-28.767015,-28.570137 +100251074.010,100.251074,-28.499910,-28.573946 +100251366.978,100.251367,-28.609785,-28.571940 +100251659.947,100.251660,-28.499849,-28.571838 +100251952.915,100.251953,-28.790968,-28.576931 +100252245.884,100.252246,-28.407928,-28.579020 +100252538.852,100.252539,-28.359674,-28.576992 +100252831.821,100.252832,-28.193993,-28.572386 +100253124.789,100.253125,-28.108873,-28.567866 +100253417.758,100.253418,-28.942743,-28.568342 +100253710.726,100.253711,-28.679256,-28.571464 +100254003.695,100.254004,-28.617949,-28.574157 +100254296.663,100.254297,-28.566366,-28.569218 +100254589.632,100.254590,-28.657660,-28.561718 +100254882.600,100.254883,-28.167171,-28.554723 +100255175.569,100.255176,-27.916006,-28.553680 +100255468.537,100.255469,-28.324432,-28.556767 +100255761.506,100.255762,-28.668270,-28.559042 +100256054.474,100.256054,-28.288061,-28.561312 +100256347.443,100.256347,-28.556028,-28.564494 +100256640.411,100.256640,-28.759235,-28.564047 +100256933.380,100.256933,-28.845480,-28.569511 +100257226.348,100.257226,-28.443018,-28.569028 +100257519.317,100.257519,-28.447037,-28.571001 +100257812.285,100.257812,-28.069172,-28.571082 +100258105.254,100.258105,-28.101126,-28.568596 +100258398.222,100.258398,-28.872673,-28.562558 +100258691.191,100.258691,-28.591351,-28.565751 +100258984.159,100.258984,-28.542290,-28.570166 +100259277.128,100.259277,-28.562468,-28.570945 +100259570.096,100.259570,-28.250872,-28.570389 +100259863.065,100.259863,-28.835550,-28.570878 +100260156.033,100.260156,-28.891544,-28.574144 +100260449.002,100.260449,-29.130737,-28.574155 +100260741.970,100.260742,-28.955511,-28.574172 +100261034.939,100.261035,-28.916929,-28.572751 +100261327.907,100.261328,-28.548315,-28.569726 +100261620.876,100.261621,-28.564070,-28.567304 +100261913.844,100.261914,-28.393988,-28.569181 +100262206.813,100.262207,-28.845781,-28.561152 +100262499.781,100.262500,-28.624069,-28.554159 +100262792.750,100.262793,-28.566456,-28.557056 +100263085.718,100.263086,-28.791582,-28.553468 +100263378.687,100.263379,-28.932632,-28.551094 +100263671.655,100.263672,-28.739033,-28.547917 +100263964.624,100.263965,-28.574966,-28.541899 +100264257.592,100.264258,-28.524397,-28.541312 +100264550.561,100.264551,-28.874079,-28.545068 +100264843.529,100.264844,-28.693209,-28.541126 +100265136.498,100.265136,-28.642946,-28.532725 +100265429.466,100.265429,-29.185682,-28.528824 +100265722.435,100.265722,-28.738804,-28.529137 +100266015.403,100.266015,-28.334585,-28.528734 +100266308.372,100.266308,-28.553484,-28.528413 +100266601.340,100.266601,-28.765245,-28.524866 +100266894.309,100.266894,-28.634382,-28.521287 +100267187.277,100.267187,-28.366930,-28.519631 +100267480.246,100.267480,-28.177649,-28.517270 +100267773.214,100.267773,-28.145849,-28.514026 +100268066.183,100.268066,-28.374557,-28.507387 +100268359.151,100.268359,-28.543993,-28.504188 +100268652.120,100.268652,-28.654606,-28.501010 +100268945.088,100.268945,-28.613344,-28.500844 +100269238.057,100.269238,-28.760595,-28.501131 +100269531.025,100.269531,-27.950401,-28.495977 +100269823.994,100.269824,-28.227457,-28.489370 +100270116.962,100.270117,-28.770735,-28.486910 +100270409.931,100.270410,-28.890207,-28.481671 +100270702.899,100.270703,-29.298422,-28.479067 +100270995.868,100.270996,-28.999949,-28.480050 +100271288.836,100.271289,-28.377563,-28.475794 +100271581.805,100.271582,-28.174725,-28.469799 +100271874.773,100.271875,-28.673208,-28.467345 +100272167.742,100.272168,-28.658457,-28.466110 +100272460.710,100.272461,-28.555021,-28.466775 +100272753.679,100.272754,-28.331873,-28.469125 +100273046.647,100.273047,-28.207182,-28.471162 +100273339.616,100.273340,-28.450111,-28.474246 +100273632.584,100.273633,-28.814234,-28.472314 +100273925.553,100.273926,-28.818365,-28.469675 +100274218.521,100.274219,-27.812599,-28.464405 +100274511.490,100.274511,-27.650114,-28.460637 +100274804.458,100.274804,-27.999147,-28.457116 +100275097.427,100.275097,-28.429405,-28.459397 +100275390.396,100.275390,-28.795286,-28.464500 +100275683.364,100.275683,-28.676548,-28.463111 +100275976.333,100.275976,-28.868210,-28.460220 +100276269.301,100.276269,-28.622782,-28.457339 +100276562.270,100.276562,-28.311251,-28.453987 +100276855.238,100.276855,-28.911024,-28.452640 +100277148.207,100.277148,-28.213940,-28.450468 +100277441.175,100.277441,-28.528978,-28.449354 +100277734.144,100.277734,-28.567593,-28.450368 +100278027.112,100.278027,-28.289907,-28.454148 +100278320.081,100.278320,-28.352816,-28.455926 +100278613.049,100.278613,-28.726818,-28.451152 +100278906.018,100.278906,-28.632219,-28.452903 +100279198.986,100.279199,-28.522261,-28.454551 +100279491.955,100.279492,-28.574522,-28.454379 +100279784.923,100.279785,-28.961988,-28.459572 +100280077.892,100.280078,-28.954290,-28.458475 +100280370.860,100.280371,-28.665297,-28.458089 +100280663.829,100.280664,-28.533882,-28.458786 +100280956.797,100.280957,-28.147879,-28.457102 +100281249.766,100.281250,-27.988903,-28.456727 +100281542.734,100.281543,-28.316084,-28.459258 +100281835.703,100.281836,-28.518702,-28.462001 +100282128.671,100.282129,-27.537975,-28.463832 +100282421.640,100.282422,-27.347353,-28.463208 +100282714.608,100.282715,-28.355967,-28.463443 +100283007.577,100.283008,-28.268839,-28.463782 +100283300.545,100.283301,-28.623573,-28.464452 +100283593.514,100.283594,-28.564306,-28.462055 +100283886.482,100.283886,-28.173651,-28.460136 +100284179.451,100.284179,-28.230536,-28.461079 +100284472.419,100.284472,-28.758060,-28.462400 +100284765.388,100.284765,-28.260321,-28.455873 +100285058.356,100.285058,-27.860674,-28.449131 +100285351.325,100.285351,-28.273993,-28.449143 +100285644.293,100.285644,-28.555208,-28.448630 +100285937.262,100.285937,-28.414122,-28.449459 +100286230.230,100.286230,-28.864241,-28.452827 +100286523.199,100.286523,-28.541952,-28.450564 +100286816.167,100.286816,-28.121780,-28.450550 +100287109.136,100.287109,-28.017399,-28.450690 +100287402.104,100.287402,-28.054619,-28.452769 +100287695.073,100.287695,-28.078714,-28.456576 +100287988.041,100.287988,-27.806168,-28.460237 +100288281.010,100.288281,-28.088478,-28.461528 +100288573.978,100.288574,-28.401293,-28.461507 +100288866.947,100.288867,-28.573816,-28.463053 +100289159.915,100.289160,-28.509153,-28.467555 +100289452.884,100.289453,-28.200865,-28.467528 +100289745.852,100.289746,-28.158884,-28.473060 +100290038.821,100.290039,-28.678324,-28.475831 +100290331.789,100.290332,-28.474855,-28.472834 +100290624.758,100.290625,-28.789028,-28.470650 +100290917.726,100.290918,-28.913431,-28.465098 +100291210.695,100.291211,-27.950129,-28.464423 +100291503.663,100.291504,-27.835459,-28.466140 +100291796.632,100.291797,-28.173809,-28.464592 +100292089.600,100.292090,-28.612421,-28.459648 +100292382.569,100.292383,-28.498663,-28.459702 +100292675.537,100.292676,-28.685734,-28.459091 +100292968.506,100.292969,-28.463987,-28.456210 +100293261.474,100.293261,-28.516039,-28.455469 +100293554.443,100.293554,-28.657339,-28.456624 +100293847.411,100.293847,-28.315586,-28.454782 +100294140.380,100.294140,-27.928316,-28.453591 +100294433.348,100.294433,-28.067408,-28.460936 +100294726.317,100.294726,-28.179955,-28.467432 +100295019.285,100.295019,-28.469700,-28.470862 +100295312.254,100.295312,-28.580908,-28.472886 +100295605.222,100.295605,-28.143934,-28.469290 +100295898.191,100.295898,-28.271673,-28.467562 +100296191.159,100.296191,-27.924175,-28.462335 +100296484.128,100.296484,-28.109770,-28.460122 +100296777.096,100.296777,-28.565655,-28.462434 +100297070.065,100.297070,-28.531385,-28.461197 +100297363.033,100.297363,-28.293144,-28.462202 +100297656.002,100.297656,-28.588259,-28.462580 +100297948.970,100.297949,-28.571186,-28.466492 +100298241.939,100.298242,-28.331591,-28.469243 +100298534.907,100.298535,-28.206232,-28.467059 +100298827.876,100.298828,-28.846392,-28.464518 +100299120.844,100.299121,-28.779181,-28.461194 +100299413.813,100.299414,-28.538382,-28.459046 +100299706.781,100.299707,-28.988525,-28.457830 +100299999.750,100.300000,-28.674721,-28.454609 +100300292.719,100.300293,-28.833517,-28.453619 +100300585.687,100.300586,-29.243559,-28.451738 +100300878.656,100.300879,-28.704184,-28.446122 +100301171.624,100.301172,-28.860287,-28.446265 +100301464.593,100.301465,-28.917624,-28.448621 +100301757.561,100.301758,-28.967068,-28.449174 +100302050.530,100.302051,-28.648129,-28.449377 +100302343.498,100.302343,-28.753445,-28.460203 +100302636.467,100.302636,-28.657503,-28.470225 +100302929.435,100.302929,-28.614279,-28.470841 +100303222.404,100.303222,-28.891756,-28.471941 +100303515.372,100.303515,-28.581394,-28.470730 +100303808.341,100.303808,-28.467449,-28.471657 +100304101.309,100.304101,-28.709661,-28.475971 +100304394.278,100.304394,-28.711967,-28.480888 +100304687.246,100.304687,-27.987242,-28.480811 +100304980.215,100.304980,-27.812548,-28.480842 +100305273.183,100.305273,-28.644640,-28.486184 +100305566.152,100.305566,-29.103327,-28.489138 +100305859.120,100.305859,-28.861858,-28.487577 +100306152.089,100.306152,-28.808722,-28.484607 +100306445.057,100.306445,-28.247221,-28.479633 +100306738.026,100.306738,-28.763144,-28.474303 +100307030.994,100.307031,-28.654402,-28.476089 +100307323.963,100.307324,-28.655636,-28.479141 +100307616.931,100.307617,-28.696861,-28.482769 +100307909.900,100.307910,-28.639627,-28.487658 +100308202.868,100.308203,-28.551468,-28.495269 +100308495.837,100.308496,-28.541044,-28.500694 +100308788.805,100.308789,-28.881880,-28.504456 +100309081.774,100.309082,-29.303495,-28.505966 +100309374.742,100.309375,-28.756693,-28.506899 +100309667.711,100.309668,-28.680868,-28.508147 +100309960.679,100.309961,-28.601831,-28.513542 +100310253.648,100.310254,-28.351995,-28.517026 +100310546.616,100.310547,-28.572638,-28.517837 +100310839.585,100.310840,-28.463234,-28.516315 +100311132.553,100.311133,-28.896572,-28.512942 +100311425.522,100.311426,-28.614428,-28.517593 +100311718.490,100.311718,-27.980713,-28.522294 +100312011.459,100.312011,-28.014799,-28.524047 +100312304.427,100.312304,-28.666155,-28.521394 +100312597.396,100.312597,-28.470232,-28.521342 +100312890.364,100.312890,-27.964527,-28.516204 +100313183.333,100.313183,-28.112307,-28.515612 +100313476.301,100.313476,-28.611061,-28.517346 +100313769.270,100.313769,-28.548521,-28.520241 +100314062.238,100.314062,-28.644470,-28.525433 +100314355.207,100.314355,-28.780241,-28.533358 +100314648.175,100.314648,-28.458784,-28.535971 +100314941.144,100.314941,-28.442675,-28.534662 +100315234.112,100.315234,-28.713171,-28.531640 +100315527.081,100.315527,-28.294355,-28.530891 +100315820.049,100.315820,-28.435133,-28.535778 +100316113.018,100.316113,-28.145975,-28.537181 +100316405.986,100.316406,-28.319006,-28.543135 +100316698.955,100.316699,-28.628496,-28.548889 +100316991.923,100.316992,-28.727007,-28.552529 +100317284.892,100.317285,-28.346062,-28.554191 +100317577.860,100.317578,-28.581812,-28.555709 +100317870.829,100.317871,-29.154305,-28.554392 +100318163.797,100.318164,-28.667683,-28.555362 +100318456.766,100.318457,-28.070730,-28.555970 +100318749.734,100.318750,-28.372122,-28.557190 +100319042.703,100.319043,-28.182745,-28.557763 +100319335.671,100.319336,-28.233526,-28.554066 +100319628.640,100.319629,-28.406687,-28.554561 +100319921.608,100.319922,-28.492596,-28.551529 +100320214.577,100.320215,-28.804663,-28.551154 +100320507.545,100.320508,-28.402746,-28.550583 +100320800.514,100.320801,-27.812658,-28.548830 +100321093.482,100.321093,-28.166222,-28.549713 +100321386.451,100.321386,-28.289522,-28.546330 +100321679.419,100.321679,-28.390160,-28.544926 +100321972.388,100.321972,-28.547087,-28.543346 +100322265.356,100.322265,-28.943678,-28.544261 +100322558.325,100.322558,-28.564495,-28.544092 +100322851.293,100.322851,-28.438856,-28.542191 +100323144.262,100.323144,-28.415203,-28.538233 +100323437.230,100.323437,-28.455019,-28.528989 +100323730.199,100.323730,-28.695974,-28.527015 +100324023.167,100.324023,-28.763603,-28.527406 +100324316.136,100.324316,-28.918436,-28.523218 +100324609.104,100.324609,-28.746788,-28.521717 +100324902.073,100.324902,-28.264418,-28.526250 +100325195.042,100.325195,-28.546152,-28.533227 +100325488.010,100.325488,-28.677408,-28.531689 +100325780.979,100.325781,-28.343325,-28.527628 +100326073.947,100.326074,-28.031252,-28.521950 +100326366.916,100.326367,-28.177439,-28.518181 +100326659.884,100.326660,-27.857632,-28.518803 +100326952.853,100.326953,-28.353321,-28.514815 +100327245.821,100.327246,-28.410740,-28.511303 +100327538.790,100.327539,-28.530329,-28.509214 +100327831.758,100.327832,-28.736069,-28.505230 +100328124.727,100.328125,-28.802702,-28.504706 +100328417.695,100.328418,-28.823702,-28.502007 +100328710.664,100.328711,-28.936491,-28.500978 +100329003.632,100.329004,-28.789129,-28.499749 +100329296.601,100.329297,-28.639088,-28.496983 +100329589.569,100.329590,-28.363155,-28.494044 +100329882.538,100.329883,-28.900259,-28.488267 +100330175.506,100.330176,-29.204552,-28.486897 +100330468.475,100.330468,-28.586273,-28.490748 +100330761.443,100.330761,-28.572521,-28.493222 +100331054.412,100.331054,-28.434362,-28.497809 +100331347.380,100.331347,-28.549026,-28.500181 +100331640.349,100.331640,-28.423677,-28.503526 +100331933.317,100.331933,-28.401228,-28.509278 +100332226.286,100.332226,-28.256397,-28.515539 +100332519.254,100.332519,-28.491510,-28.516601 +100332812.223,100.332812,-28.009789,-28.515881 +100333105.191,100.333105,-28.384535,-28.517147 +100333398.160,100.333398,-28.760271,-28.520033 +100333691.128,100.333691,-29.087429,-28.523347 +100333984.097,100.333984,-29.052996,-28.528380 +100334277.065,100.334277,-28.994524,-28.533178 +100334570.034,100.334570,-28.401281,-28.534679 +100334863.002,100.334863,-28.017818,-28.536941 +100335155.971,100.335156,-28.079723,-28.540366 +100335448.939,100.335449,-28.478397,-28.540113 +100335741.908,100.335742,-28.803596,-28.543752 +100336034.876,100.336035,-28.456503,-28.547240 +100336327.845,100.336328,-28.696766,-28.549867 +100336620.813,100.336621,-28.888651,-28.550440 +100336913.782,100.336914,-29.097244,-28.548450 +100337206.750,100.337207,-28.764067,-28.544010 +100337499.719,100.337500,-28.493475,-28.543066 +100337792.687,100.337793,-28.410109,-28.540682 +100338085.656,100.338086,-28.706656,-28.533667 +100338378.624,100.338379,-28.411428,-28.529018 +100338671.593,100.338672,-28.363220,-28.531734 +100338964.561,100.338965,-28.931185,-28.534270 +100339257.530,100.339258,-28.276302,-28.535663 +100339550.498,100.339550,-28.606434,-28.538557 +100339843.467,100.339843,-28.552067,-28.540767 +100340136.435,100.340136,-28.622055,-28.539314 +100340429.404,100.340429,-28.750946,-28.534245 +100340722.372,100.340722,-28.970436,-28.534353 +100341015.341,100.341015,-28.831514,-28.539115 +100341308.309,100.341308,-28.388729,-28.546945 +100341601.278,100.341601,-28.713022,-28.552954 +100341894.246,100.341894,-28.734793,-28.554466 +100342187.215,100.342187,-28.778419,-28.553412 +100342480.183,100.342480,-28.729214,-28.549209 +100342773.152,100.342773,-28.398029,-28.547527 +100343066.120,100.343066,-28.095041,-28.548266 +100343359.089,100.343359,-27.697021,-28.548580 +100343652.057,100.343652,-28.315836,-28.549804 +100343945.026,100.343945,-28.520628,-28.552472 +100344237.994,100.344238,-28.148848,-28.551363 +100344530.963,100.344531,-28.502247,-28.548946 +100344823.931,100.344824,-28.574011,-28.544274 +100345116.900,100.345117,-28.709452,-28.545664 +100345409.868,100.345410,-28.433657,-28.546188 +100345702.837,100.345703,-28.510670,-28.545718 +100345995.805,100.345996,-28.093462,-28.547405 +100346288.774,100.346289,-28.289185,-28.549336 +100346581.742,100.346582,-28.328016,-28.551455 +100346874.711,100.346875,-28.220221,-28.558514 +100347167.679,100.347168,-28.183889,-28.561911 +100347460.648,100.347461,-28.369335,-28.564272 +100347753.616,100.347754,-28.161062,-28.563265 +100348046.585,100.348047,-28.566290,-28.561552 +100348339.553,100.348340,-28.192793,-28.558397 +100348632.522,100.348633,-28.401068,-28.551691 +100348925.490,100.348925,-28.701872,-28.544555 +100349218.459,100.349218,-28.869629,-28.540307 +100349511.427,100.349511,-28.348942,-28.534569 +100349804.396,100.349804,-27.923965,-28.532945 +100350097.365,100.350097,-28.413282,-28.527472 +100350390.333,100.350390,-28.895931,-28.522959 +100350683.302,100.350683,-28.932425,-28.524027 +100350976.270,100.350976,-29.136633,-28.526350 +100351269.239,100.351269,-29.268167,-28.527993 +100351562.207,100.351562,-29.112234,-28.525152 +100351855.176,100.351855,-28.741777,-28.526163 +100352148.144,100.352148,-28.856203,-28.526830 +100352441.113,100.352441,-28.819468,-28.525503 +100352734.081,100.352734,-28.373751,-28.523344 +100353027.050,100.353027,-28.120005,-28.526507 +100353320.018,100.353320,-28.488119,-28.523629 +100353612.987,100.353613,-29.101162,-28.516435 +100353905.955,100.353906,-29.303766,-28.509238 +100354198.924,100.354199,-29.377785,-28.505527 +100354491.892,100.354492,-29.003452,-28.499597 +100354784.861,100.354785,-28.774483,-28.495112 +100355077.829,100.355078,-28.927673,-28.499017 +100355370.798,100.355371,-28.677332,-28.499685 +100355663.766,100.355664,-28.792831,-28.495747 +100355956.735,100.355957,-28.927784,-28.492481 +100356249.703,100.356250,-28.487072,-28.493143 +100356542.672,100.356543,-28.394196,-28.489833 +100356835.640,100.356836,-28.359327,-28.483588 +100357128.609,100.357129,-28.135130,-28.474045 +100357421.577,100.357422,-28.224167,-28.467700 +100357714.546,100.357715,-28.264143,-28.463104 +100358007.514,100.358008,-28.167521,-28.464710 +100358300.483,100.358300,-28.055737,-28.464093 +100358593.451,100.358593,-28.418970,-28.462200 +100358886.420,100.358886,-28.720560,-28.460780 +100359179.388,100.359179,-28.362392,-28.457811 +100359472.357,100.359472,-28.619925,-28.461494 +100359765.325,100.359765,-28.710802,-28.461196 +100360058.294,100.360058,-28.299986,-28.456613 +100360351.262,100.360351,-28.122559,-28.454697 +100360644.231,100.360644,-28.416998,-28.455509 +100360937.199,100.360937,-28.403341,-28.454426 +100361230.168,100.361230,-29.281496,-28.448608 +100361523.136,100.361523,-29.143543,-28.447486 +100361816.105,100.361816,-28.594303,-28.442026 +100362109.073,100.362109,-28.405264,-28.435673 +100362402.042,100.362402,-28.355202,-28.429803 +100362695.010,100.362695,-28.339169,-28.429320 +100362987.979,100.362988,-28.538761,-28.431697 +100363280.947,100.363281,-28.457136,-28.433256 +100363573.916,100.363574,-28.622164,-28.438719 +100363866.884,100.363867,-29.091398,-28.437821 +100364159.853,100.364160,-28.606951,-28.435099 +100364452.821,100.364453,-28.572140,-28.434744 +100364745.790,100.364746,-28.123512,-28.430331 +100365038.758,100.365039,-28.446779,-28.426599 +100365331.727,100.365332,-28.618494,-28.426289 +100365624.695,100.365625,-28.611563,-28.429770 +100365917.664,100.365918,-28.569595,-28.429567 +100366210.632,100.366211,-28.272709,-28.429045 +100366503.601,100.366504,-28.452387,-28.428011 +100366796.569,100.366797,-28.770849,-28.424998 +100367089.538,100.367090,-28.821518,-28.426023 +100367382.506,100.367383,-28.734777,-28.428364 +100367675.475,100.367675,-28.395550,-28.427622 +100367968.443,100.367968,-28.498463,-28.424973 +100368261.412,100.368261,-28.368120,-28.423206 +100368554.380,100.368554,-27.942167,-28.424958 +100368847.349,100.368847,-27.980692,-28.423737 +100369140.317,100.369140,-28.213104,-28.423426 +100369433.286,100.369433,-27.900835,-28.420317 +100369726.254,100.369726,-28.154377,-28.417159 +100370019.223,100.370019,-28.152008,-28.418792 +100370312.191,100.370312,-28.535860,-28.422092 +100370605.160,100.370605,-28.737322,-28.420971 +100370898.128,100.370898,-28.906776,-28.418459 +100371191.097,100.371191,-28.660479,-28.413926 +100371484.065,100.371484,-28.174412,-28.405393 +100371777.034,100.371777,-28.561159,-28.400043 +100372070.002,100.372070,-28.490917,-28.397608 +100372362.971,100.372363,-28.088755,-28.393810 +100372655.939,100.372656,-28.207521,-28.391784 +100372948.908,100.372949,-28.412817,-28.392465 +100373241.876,100.373242,-28.018671,-28.394419 +100373534.845,100.373535,-27.826542,-28.393654 +100373827.813,100.373828,-28.086664,-28.389952 +100374120.782,100.374121,-28.511690,-28.383240 +100374413.750,100.374414,-28.168493,-28.372537 +100374706.719,100.374707,-27.838400,-28.366293 +100374999.688,100.375000,-28.525446,-28.363055 +100375292.656,100.375293,-28.163635,-28.354410 +100375585.625,100.375586,-27.971928,-28.349358 +100375878.593,100.375879,-28.347963,-28.344062 +100376171.562,100.376172,-28.547396,-28.335491 +100376464.530,100.376465,-28.245556,-28.330646 +100376757.499,100.376757,-28.038940,-28.329142 +100377050.467,100.377050,-27.803185,-28.325298 +100377343.436,100.377343,-27.921385,-28.327831 +100377636.404,100.377636,-27.901987,-28.329698 +100377929.373,100.377929,-28.633043,-28.329395 +100378222.341,100.378222,-28.618181,-28.331838 +100378515.310,100.378515,-28.162386,-28.334883 +100378808.278,100.378808,-28.177080,-28.329005 +100379101.247,100.379101,-28.499949,-28.321219 +100379394.215,100.379394,-28.789045,-28.318278 +100379687.184,100.379687,-28.564533,-28.317555 +100379980.152,100.379980,-27.953655,-28.313520 +100380273.121,100.380273,-28.357557,-28.311529 +100380566.089,100.380566,-28.871756,-28.307791 +100380859.058,100.380859,-28.806532,-28.302805 +100381152.026,100.381152,-28.038496,-28.298978 +100381444.995,100.381445,-28.239731,-28.288509 +100381737.963,100.381738,-27.982430,-28.279028 +100382030.932,100.382031,-27.890638,-28.273825 +100382323.900,100.382324,-27.984781,-28.269371 +100382616.869,100.382617,-28.658865,-28.268547 +100382909.837,100.382910,-28.733906,-28.264425 +100383202.806,100.383203,-28.297308,-28.255073 +100383495.774,100.383496,-28.379232,-28.247565 +100383788.743,100.383789,-28.197805,-28.234098 +100384081.711,100.384082,-28.156216,-28.212716 +100384374.680,100.384375,-28.103466,-28.194057 +100384667.648,100.384668,-27.927622,-28.178997 +100384960.617,100.384961,-28.075098,-28.176418 +100385253.585,100.385254,-28.664385,-28.168347 +100385546.554,100.385547,-28.939528,-28.154581 +100385839.522,100.385840,-28.482412,-28.140369 +100386132.491,100.386132,-28.027746,-28.128272 +100386425.459,100.386425,-28.154211,-28.115773 +100386718.428,100.386718,-27.942053,-28.097464 +100387011.396,100.387011,-28.356308,-28.073236 +100387304.365,100.387304,-28.498255,-28.050428 +100387597.333,100.387597,-28.270155,-28.029256 +100387890.302,100.387890,-27.832418,-28.002904 +100388183.270,100.388183,-28.323164,-27.976159 +100388476.239,100.388476,-28.426441,-27.955290 +100388769.207,100.388769,-28.237839,-27.937143 +100389062.176,100.389062,-28.656672,-27.910670 +100389355.144,100.389355,-28.421412,-27.877050 +100389648.113,100.389648,-27.942766,-27.843266 +100389941.081,100.389941,-28.128393,-27.810826 +100390234.050,100.390234,-28.889788,-27.780103 +100390527.018,100.390527,-28.727961,-27.753517 +100390819.987,100.390820,-28.561419,-27.719311 +100391112.955,100.391113,-28.459337,-27.681254 +100391405.924,100.391406,-28.036127,-27.636135 +100391698.892,100.391699,-28.324984,-27.594582 +100391991.861,100.391992,-28.395046,-27.544444 +100392284.829,100.392285,-28.313065,-27.496469 +100392577.798,100.392578,-28.523628,-27.444719 +100392870.766,100.392871,-28.467697,-27.395462 +100393163.735,100.393164,-28.378822,-27.338560 +100393456.703,100.393457,-28.382313,-27.275121 +100393749.672,100.393750,-28.541677,-27.206834 +100394042.640,100.394043,-28.294851,-27.130007 +100394335.609,100.394336,-27.839518,-27.039409 +100394628.577,100.394629,-28.111662,-26.965393 +100394921.546,100.394922,-28.312561,-26.895575 +100395214.514,100.395215,-27.752020,-26.817002 +100395507.483,100.395507,-27.988894,-26.716984 +100395800.451,100.395800,-28.055096,-26.638490 +100396093.420,100.396093,-27.756519,-26.559347 +100396386.388,100.396386,-27.848644,-26.460872 +100396679.357,100.396679,-28.189995,-26.356300 +100396972.325,100.396972,-27.858746,-26.249223 +100397265.294,100.397265,-28.480534,-26.153439 +100397558.262,100.397558,-28.481394,-26.068761 +100397851.231,100.397851,-28.223410,-25.982700 +100398144.199,100.398144,-28.502447,-25.891804 +100398437.168,100.398437,-28.465847,-25.792351 +100398730.136,100.398730,-27.666464,-25.665012 +100399023.105,100.399023,-27.685926,-25.532811 +100399316.073,100.399316,-27.973372,-25.424113 +100399609.042,100.399609,-28.515116,-25.314958 +100399902.010,100.399902,-28.143883,-25.200318 +100400194.979,100.400195,-28.036295,-25.047556 +100400487.948,100.400488,-27.657728,-24.877850 +100400780.916,100.400781,-27.767296,-24.743558 +100401073.885,100.401074,-27.897186,-24.610739 +100401366.853,100.401367,-27.775187,-24.461819 +100401659.822,100.401660,-27.793612,-24.332260 +100401952.790,100.401953,-27.888109,-24.197997 +100402245.759,100.402246,-27.817495,-24.050389 +100402538.727,100.402539,-28.241648,-23.883622 +100402831.696,100.402832,-27.799845,-23.709414 +100403124.664,100.403125,-27.350805,-23.560298 +100403417.633,100.403418,-27.493832,-23.415778 +100403710.601,100.403711,-26.967310,-23.243091 +100404003.570,100.404004,-26.481848,-23.072103 +100404296.538,100.404297,-26.442823,-22.916367 +100404589.507,100.404590,-26.756041,-22.768371 +100404882.475,100.404882,-27.787863,-22.600654 +100405175.444,100.405175,-27.404617,-22.420974 +100405468.412,100.405468,-26.908659,-22.230295 +100405761.381,100.405761,-26.853935,-22.045971 +100406054.349,100.406054,-27.041115,-21.850226 +100406347.318,100.406347,-26.786028,-21.630789 +100406640.286,100.406640,-26.342714,-21.422332 +100406933.255,100.406933,-25.994825,-21.216994 +100407226.223,100.407226,-26.133402,-21.019089 +100407519.192,100.407519,-26.221355,-20.826265 +100407812.160,100.407812,-25.591570,-20.645611 +100408105.129,100.408105,-25.601339,-20.446202 +100408398.097,100.408398,-26.007160,-20.191000 +100408691.066,100.408691,-25.977833,-19.932735 +100408984.034,100.408984,-25.318213,-19.693000 +100409277.003,100.409277,-24.905970,-19.459756 +100409569.971,100.409570,-24.729074,-19.185529 +100409862.940,100.409863,-24.925055,-18.892366 +100410155.908,100.410156,-25.030039,-18.648478 +100410448.877,100.410449,-25.523054,-18.415907 +100410741.845,100.410742,-25.010592,-18.193748 +100411034.814,100.411035,-24.782232,-17.985009 +100411327.782,100.411328,-24.200418,-17.757486 +100411620.751,100.411621,-24.210863,-17.489827 +100411913.719,100.411914,-23.808306,-17.258474 +100412206.688,100.412207,-23.878426,-17.039519 +100412499.656,100.412500,-23.491560,-16.824258 +100412792.625,100.412793,-23.639692,-16.617363 +100413085.593,100.413086,-23.249098,-16.433418 +100413378.562,100.413379,-22.758324,-16.240342 +100413671.530,100.413672,-22.429394,-16.037488 +100413964.499,100.413964,-22.072382,-15.843474 +100414257.467,100.414257,-21.548361,-15.639284 +100414550.436,100.414550,-22.089607,-15.438470 +100414843.404,100.414843,-22.139307,-15.241737 +100415136.373,100.415136,-21.860029,-15.056316 +100415429.341,100.415429,-20.878487,-14.888101 +100415722.310,100.415722,-21.601233,-14.704645 +100416015.278,100.416015,-21.595037,-14.513029 +100416308.247,100.416308,-20.790621,-14.329967 +100416601.215,100.416601,-20.438255,-14.172858 +100416894.184,100.416894,-20.229990,-14.024275 +100417187.152,100.417187,-20.503925,-13.893545 +100417480.121,100.417480,-20.886166,-13.775184 +100417773.089,100.417773,-20.757084,-13.647822 +100418066.058,100.418066,-20.616140,-13.529836 +100418359.026,100.418359,-20.201267,-13.416571 +100418651.995,100.418652,-19.112368,-13.294160 +100418944.963,100.418945,-18.856905,-13.178894 +100419237.932,100.419238,-19.537046,-13.057058 +100419530.900,100.419531,-19.460600,-12.930353 +100419823.869,100.419824,-19.147354,-12.816909 +100420116.837,100.420117,-17.848166,-12.717231 +100420409.806,100.420410,-17.319899,-12.626340 +100420702.774,100.420703,-18.155813,-12.542801 +100420995.743,100.420996,-18.071880,-12.459012 +100421288.711,100.421289,-17.422903,-12.375515 +100421581.680,100.421582,-17.864990,-12.292843 +100421874.648,100.421875,-17.580864,-12.214925 +100422167.617,100.422168,-17.068386,-12.142026 +100422460.585,100.422461,-16.440723,-12.068263 +100422753.554,100.422754,-16.145613,-11.987238 +100423046.522,100.423047,-16.633682,-11.898454 +100423339.491,100.423339,-16.595308,-11.824849 +100423632.459,100.423632,-15.724862,-11.744665 +100423925.428,100.423925,-15.593094,-11.660781 +100424218.396,100.424218,-15.819466,-11.580289 +100424511.365,100.424511,-15.881796,-11.501437 +100424804.333,100.424804,-15.210360,-11.417967 +100425097.302,100.425097,-14.768435,-11.339322 +100425390.271,100.425390,-14.368298,-11.249444 +100425683.239,100.425683,-14.339372,-11.167531 +100425976.208,100.425976,-13.888021,-11.087754 +100426269.176,100.426269,-13.192531,-10.996665 +100426562.145,100.426562,-13.205382,-10.907880 +100426855.113,100.426855,-13.061678,-10.830116 +100427148.082,100.427148,-13.034317,-10.752830 +100427441.050,100.427441,-12.958329,-10.675668 +100427734.019,100.427734,-13.045391,-10.596085 +100428026.987,100.428027,-12.431643,-10.509248 +100428319.956,100.428320,-11.175197,-10.422519 +100428612.924,100.428613,-10.874229,-10.340252 +100428905.893,100.428906,-10.943888,-10.254735 +100429198.861,100.429199,-10.836063,-10.170334 +100429491.830,100.429492,-9.890112,-10.089660 +100429784.798,100.429785,-9.317656,-10.005088 +100430077.767,100.430078,-9.843637,-9.925159 +100430370.735,100.430371,-9.822577,-9.846636 +100430663.704,100.430664,-9.792513,-9.763277 +100430956.672,100.430957,-9.844810,-9.688981 +100431249.641,100.431250,-9.258614,-9.606365 +100431542.609,100.431543,-8.311269,-9.513848 +100431835.578,100.431836,-8.693973,-9.427651 +100432128.546,100.432129,-8.708697,-9.349881 +100432421.515,100.432422,-8.566146,-9.267078 +100432714.483,100.432714,-8.529830,-9.190779 +100433007.452,100.433007,-8.841239,-9.104872 +100433300.420,100.433300,-8.445537,-9.013697 +100433593.389,100.433593,-8.037030,-8.938952 +100433886.357,100.433886,-8.033611,-8.866821 +100434179.326,100.434179,-7.613995,-8.789104 +100434472.294,100.434472,-7.481015,-8.717037 +100434765.263,100.434765,-7.374798,-8.647533 +100435058.231,100.435058,-7.442046,-8.581442 +100435351.200,100.435351,-7.680982,-8.513866 +100435644.168,100.435644,-7.135550,-8.442708 +100435937.137,100.435937,-6.762444,-8.372571 +100436230.105,100.436230,-6.771071,-8.302110 +100436523.074,100.436523,-7.261683,-8.231252 +100436816.042,100.436816,-7.353360,-8.163660 +100437109.011,100.437109,-7.762931,-8.096076 +100437401.979,100.437402,-8.073058,-8.027976 +100437694.948,100.437695,-7.635699,-7.964509 +100437987.916,100.437988,-7.841059,-7.898500 +100438280.885,100.438281,-7.904751,-7.825522 +100438573.853,100.438574,-7.453139,-7.759451 +100438866.822,100.438867,-7.587151,-7.690577 +100439159.790,100.439160,-7.231412,-7.621970 +100439452.759,100.439453,-6.941890,-7.556630 +100439745.727,100.439746,-7.303354,-7.494780 +100440038.696,100.440039,-7.751729,-7.440169 +100440331.664,100.440332,-8.053338,-7.388918 +100440624.633,100.440625,-8.325305,-7.337838 +100440917.601,100.440918,-8.231021,-7.284092 +100441210.570,100.441211,-8.164668,-7.220627 +100441503.538,100.441504,-8.123867,-7.160392 +100441796.507,100.441797,-8.298998,-7.104932 +100442089.475,100.442089,-8.511443,-7.047375 +100442382.444,100.442382,-8.387537,-6.990883 +100442675.412,100.442675,-7.911691,-6.936852 +100442968.381,100.442968,-7.429976,-6.876317 +100443261.349,100.443261,-8.150640,-6.805485 +100443554.318,100.443554,-7.708587,-6.734126 +100443847.286,100.443847,-7.427930,-6.672811 +100444140.255,100.444140,-7.518140,-6.610368 +100444433.223,100.444433,-7.527206,-6.548999 +100444726.192,100.444726,-7.206521,-6.484317 +100445019.160,100.445019,-7.392630,-6.421919 +100445312.129,100.445312,-6.731081,-6.365019 +100445605.097,100.445605,-7.040511,-6.307565 +100445898.066,100.445898,-7.073524,-6.248013 +100446191.034,100.446191,-6.420304,-6.186605 +100446484.003,100.446484,-6.439159,-6.126969 +100446776.971,100.446777,-6.921859,-6.068012 +100447069.940,100.447070,-6.867640,-6.003392 +100447362.908,100.447363,-6.799848,-5.927810 +100447655.877,100.447656,-6.590704,-5.853702 +100447948.845,100.447949,-6.126792,-5.787311 +100448241.814,100.448242,-6.046409,-5.727914 +100448534.782,100.448535,-6.194148,-5.666612 +100448827.751,100.448828,-5.944238,-5.596655 +100449120.719,100.449121,-5.909569,-5.534630 +100449413.688,100.449414,-6.016757,-5.476981 +100449706.656,100.449707,-5.730669,-5.418890 +100449999.625,100.450000,-5.894034,-5.357845 +100450292.594,100.450293,-5.893195,-5.297236 +100450585.562,100.450586,-5.561954,-5.232242 +100450878.531,100.450879,-5.972497,-5.163669 +100451171.499,100.451171,-5.436814,-5.097417 +100451464.468,100.451464,-4.857634,-5.031663 +100451757.436,100.451757,-5.073091,-4.970449 +100452050.405,100.452050,-5.427736,-4.914690 +100452343.373,100.452343,-5.080925,-4.860140 +100452636.342,100.452636,-5.347215,-4.801079 +100452929.310,100.452929,-4.761509,-4.745324 +100453222.279,100.453222,-4.413915,-4.684991 +100453515.247,100.453515,-5.174646,-4.635608 +100453808.216,100.453808,-5.248327,-4.586027 +100454101.184,100.454101,-4.850293,-4.528258 +100454394.153,100.454394,-5.087056,-4.470760 +100454687.121,100.454687,-5.183331,-4.420529 +100454980.090,100.454980,-5.332109,-4.371013 +100455273.058,100.455273,-5.166333,-4.318363 +100455566.027,100.455566,-4.856717,-4.265569 +100455858.995,100.455859,-4.865651,-4.214293 +100456151.964,100.456152,-4.777151,-4.163019 +100456444.932,100.456445,-4.666064,-4.109517 +100456737.901,100.456738,-4.789163,-4.059006 +100457030.869,100.457031,-4.718236,-4.010012 +100457323.838,100.457324,-4.627438,-3.961132 +100457616.806,100.457617,-4.871205,-3.911945 +100457909.775,100.457910,-4.638616,-3.861412 +100458202.743,100.458203,-4.141784,-3.813365 +100458495.712,100.458496,-4.484883,-3.771213 +100458788.680,100.458789,-4.212206,-3.724919 +100459081.649,100.459082,-4.153588,-3.678507 +100459374.617,100.459375,-4.315601,-3.637061 +100459667.586,100.459668,-4.482647,-3.598136 +100459960.554,100.459961,-4.937369,-3.554080 +100460253.523,100.460254,-5.091550,-3.505738 +100460546.491,100.460546,-5.027491,-3.454602 +100460839.460,100.460839,-4.812201,-3.401780 +100461132.428,100.461132,-4.061614,-3.356553 +100461425.397,100.461425,-4.191172,-3.308059 +100461718.365,100.461718,-4.498679,-3.256818 +100462011.334,100.462011,-4.277854,-3.206843 +100462304.302,100.462304,-4.275400,-3.157150 +100462597.271,100.462597,-4.369688,-3.107311 +100462890.239,100.462890,-3.834295,-3.055016 +100463183.208,100.463183,-3.149950,-3.003036 +100463476.176,100.463476,-3.049564,-2.956691 +100463769.145,100.463769,-3.565875,-2.915934 +100464062.113,100.464062,-3.425357,-2.876439 +100464355.082,100.464355,-3.451440,-2.834515 +100464648.050,100.464648,-3.179842,-2.794220 +100464941.019,100.464941,-3.227774,-2.753559 +100465233.987,100.465234,-3.514754,-2.714456 +100465526.956,100.465527,-3.392184,-2.673692 +100465819.924,100.465820,-3.192418,-2.621676 +100466112.893,100.466113,-2.978339,-2.573650 +100466405.861,100.466406,-2.977895,-2.531975 +100466698.830,100.466699,-2.970490,-2.490612 +100466991.798,100.466992,-2.539403,-2.452584 +100467284.767,100.467285,-1.848249,-2.411939 +100467577.735,100.467578,-1.852253,-2.367330 +100467870.704,100.467871,-2.233943,-2.322272 +100468163.672,100.468164,-2.558621,-2.275901 +100468456.641,100.468457,-2.239488,-2.229237 +100468749.609,100.468750,-1.641964,-2.183159 +100469042.578,100.469043,-2.050627,-2.139959 +100469335.546,100.469336,-2.259632,-2.097173 +100469628.515,100.469629,-2.039361,-2.052328 +100469921.483,100.469921,-1.714120,-2.007617 +100470214.452,100.470214,-1.776000,-1.964210 +100470507.420,100.470507,-1.461886,-1.925179 +100470800.389,100.470800,-1.200629,-1.879983 +100471093.357,100.471093,-1.278616,-1.835740 +100471386.326,100.471386,-1.164077,-1.784135 +100471679.294,100.471679,-1.203130,-1.733355 +100471972.263,100.471972,-1.557011,-1.687585 +100472265.231,100.472265,-1.592101,-1.645768 +100472558.200,100.472558,-1.239414,-1.600685 +100472851.168,100.472851,-1.388920,-1.565150 +100473144.137,100.473144,-1.116864,-1.533952 +100473437.105,100.473437,-1.702203,-1.500951 +100473730.074,100.473730,-1.560710,-1.467117 +100474023.042,100.474023,-0.994094,-1.437639 +100474316.011,100.474316,-0.878524,-1.403570 +100474608.979,100.474609,-1.261473,-1.366179 +100474901.948,100.474902,-1.245260,-1.336636 +100475194.917,100.475195,-1.018376,-1.303457 +100475487.885,100.475488,-1.021120,-1.271169 +100475780.854,100.475781,-0.954293,-1.242230 +100476073.822,100.476074,-0.823340,-1.213666 +100476366.791,100.476367,-0.647586,-1.184848 +100476659.759,100.476660,-0.915083,-1.156855 +100476952.728,100.476953,-0.999853,-1.130529 +100477245.696,100.477246,-1.060812,-1.100032 +100477538.665,100.477539,-1.064296,-1.069765 +100477831.633,100.477832,-0.842001,-1.038562 +100478124.602,100.478125,-1.019225,-1.011784 +100478417.570,100.478418,-1.442992,-0.987343 +100478710.539,100.478711,-0.991565,-0.958196 +100479003.507,100.479004,-0.976745,-0.930667 +100479296.476,100.479296,-1.235810,-0.906797 +100479589.444,100.479589,-1.330942,-0.881920 +100479882.413,100.479882,-0.990469,-0.856379 +100480175.381,100.480175,-0.734920,-0.828441 +100480468.350,100.480468,-0.553212,-0.798840 +100480761.318,100.480761,-0.439394,-0.773041 +100481054.287,100.481054,-0.939135,-0.748984 +100481347.255,100.481347,-0.640758,-0.722451 +100481640.224,100.481640,-0.394592,-0.695093 +100481933.192,100.481933,-0.472088,-0.669583 +100482226.161,100.482226,-0.485230,-0.644205 +100482519.129,100.482519,-0.412883,-0.619700 +100482812.098,100.482812,-0.117853,-0.589895 +100483105.066,100.483105,-0.012772,-0.566292 +100483398.035,100.483398,-0.509924,-0.548359 +100483691.003,100.483691,-0.843952,-0.527772 +100483983.972,100.483984,-0.860122,-0.504470 +100484276.940,100.484277,-0.643243,-0.485142 +100484569.909,100.484570,-0.748807,-0.466709 +100484862.877,100.484863,-0.617434,-0.449335 +100485155.846,100.485156,-0.759574,-0.427413 +100485448.814,100.485449,-0.439233,-0.407596 +100485741.783,100.485742,0.372449,-0.391572 +100486034.751,100.486035,0.124925,-0.373251 +100486327.720,100.486328,-0.184682,-0.355673 +100486620.688,100.486621,-0.182321,-0.339324 +100486913.657,100.486914,-0.544515,-0.323321 +100487206.625,100.487207,-0.277727,-0.306791 +100487499.594,100.487500,0.088713,-0.295342 +100487792.562,100.487793,0.202056,-0.281427 +100488085.531,100.488086,0.435602,-0.267493 +100488378.499,100.488378,0.510914,-0.252013 +100488671.468,100.488671,0.472000,-0.237303 +100488964.436,100.488964,0.345881,-0.223485 +100489257.405,100.489257,0.354857,-0.206426 +100489550.373,100.489550,0.521461,-0.187368 +100489843.342,100.489843,0.610965,-0.166791 +100490136.310,100.490136,0.508366,-0.152409 +100490429.279,100.490429,0.188077,-0.141408 +100490722.247,100.490722,0.783940,-0.134175 +100491015.216,100.491015,0.654633,-0.127889 +100491308.184,100.491308,1.332411,-0.119757 +100491601.153,100.491601,1.446440,-0.110004 +100491894.121,100.491894,1.088183,-0.099762 +100492187.090,100.492187,0.738217,-0.089240 +100492480.058,100.492480,1.102716,-0.082126 +100492773.027,100.492773,0.295902,-0.075994 +100493065.995,100.493066,0.098352,-0.069253 +100493358.964,100.493359,0.397924,-0.062622 +100493651.932,100.493652,0.261779,-0.052691 +100493944.901,100.493945,-0.158133,-0.041221 +100494237.869,100.494238,0.422588,-0.032654 +100494530.838,100.494531,0.672267,-0.029126 +100494823.806,100.494824,-0.061697,-0.023531 +100495116.775,100.495117,0.274574,-0.016826 +100495409.743,100.495410,0.259598,-0.013467 +100495702.712,100.495703,0.044786,-0.012666 +100495995.680,100.495996,0.022990,-0.011437 +100496288.649,100.496289,0.097041,-0.010764 +100496581.617,100.496582,0.068365,-0.010044 +100496874.586,100.496875,-0.131534,-0.010702 +100497167.554,100.497168,0.340586,-0.008496 +100497460.523,100.497461,0.367411,-0.006042 +100497753.491,100.497753,0.405700,-0.006016 +100498046.460,100.498046,0.046577,-0.008739 +100498339.428,100.498339,-0.009504,-0.006842 +100498632.397,100.498632,0.378215,-0.000550 +100498925.365,100.498925,0.325439,0.000000 +100499218.334,100.499218,-0.027528,-0.000647 +100499511.302,100.499511,0.038593,-0.000362 +100499804.271,100.499804,0.068047,-0.000594 +100500097.240,100.500097,0.198406,-0.001052 +100500390.208,100.500390,0.349781,-0.002548 +100500683.177,100.500683,-0.028963,-0.006427 +100500976.145,100.500976,-0.145188,-0.011668 +100501269.114,100.501269,0.410234,-0.011469 +100501562.082,100.501562,0.468997,-0.011568 +100501855.051,100.501855,0.181300,-0.012125 +100502148.019,100.502148,0.259742,-0.011910 +100502440.988,100.502441,0.178528,-0.010130 +100502733.956,100.502734,0.732106,-0.012050 +100503026.925,100.503027,0.275013,-0.016492 +100503319.893,100.503320,-0.080044,-0.023156 +100503612.862,100.503613,0.290839,-0.025803 +100503905.830,100.503906,0.384798,-0.024608 +100504198.799,100.504199,-0.006529,-0.023702 +100504491.767,100.504492,-0.119703,-0.025685 +100504784.736,100.504785,-0.114033,-0.029612 +100505077.704,100.505078,0.416807,-0.033410 +100505370.673,100.505371,0.056053,-0.035359 +100505663.641,100.505664,-0.367053,-0.038372 +100505956.610,100.505957,0.037665,-0.047227 +100506249.578,100.506250,0.058452,-0.055068 +100506542.547,100.506543,-0.088436,-0.064110 +100506835.515,100.506836,-0.121829,-0.071950 +100507128.484,100.507128,0.179634,-0.076955 +100507421.452,100.507421,-0.042035,-0.081644 +100507714.421,100.507714,0.267522,-0.086909 +100508007.389,100.508007,0.045335,-0.095398 +100508300.358,100.508300,0.063797,-0.108340 +100508593.326,100.508593,0.152399,-0.120130 +100508886.295,100.508886,0.408120,-0.130268 +100509179.263,100.509179,0.553019,-0.139789 +100509472.232,100.509472,0.677751,-0.147812 +100509765.200,100.509765,0.963677,-0.157517 +100510058.169,100.510058,0.457907,-0.169533 +100510351.137,100.510351,-0.000600,-0.181836 +100510644.106,100.510644,-0.290833,-0.193342 +100510937.074,100.510937,-0.218788,-0.209188 +100511230.043,100.511230,-0.024961,-0.225971 +100511523.011,100.511523,0.271519,-0.245721 +100511815.980,100.511816,0.307657,-0.265597 +100512108.948,100.512109,0.100889,-0.283721 +100512401.917,100.512402,-0.394728,-0.302007 +100512694.885,100.512695,-0.261088,-0.318989 +100512987.854,100.512988,-0.289982,-0.329000 +100513280.822,100.513281,-0.090993,-0.341136 +100513573.791,100.513574,-0.066125,-0.354999 +100513866.759,100.513867,0.236808,-0.367067 +100514159.728,100.514160,0.266926,-0.377133 +100514452.696,100.514453,-0.327558,-0.391258 +100514745.665,100.514746,-0.346370,-0.407845 +100515038.633,100.515039,-0.171574,-0.417407 +100515331.602,100.515332,-0.474220,-0.429654 +100515624.570,100.515625,-0.884935,-0.443637 +100515917.539,100.515918,-0.749943,-0.458101 +100516210.507,100.516211,-0.713639,-0.472019 +100516503.476,100.516503,-0.534767,-0.487842 +100516796.444,100.516796,-1.027515,-0.503138 +100517089.413,100.517089,-0.636011,-0.516317 +100517382.381,100.517382,-0.652150,-0.535454 +100517675.350,100.517675,-1.059793,-0.555595 +100517968.318,100.517968,-1.319365,-0.573514 +100518261.287,100.518261,-0.702916,-0.588827 +100518554.255,100.518554,-0.382836,-0.603647 +100518847.224,100.518847,-0.897972,-0.624619 +100519140.192,100.519140,-1.089125,-0.644934 +100519433.161,100.519433,-1.184358,-0.661471 +100519726.129,100.519726,-1.374244,-0.678528 +100520019.098,100.520019,-1.070045,-0.697156 +100520312.066,100.520312,-0.984487,-0.715988 +100520605.035,100.520605,-1.202558,-0.733937 +100520898.003,100.520898,-1.314696,-0.748900 +100521190.972,100.521191,-0.905454,-0.763579 +100521483.940,100.521484,-0.656535,-0.782957 +100521776.909,100.521777,-0.478735,-0.803807 +100522069.877,100.522070,-0.439457,-0.825973 +100522362.846,100.522363,-0.221361,-0.850085 +100522655.814,100.522656,-0.711494,-0.869944 +100522948.783,100.522949,-0.790647,-0.894371 +100523241.751,100.523242,-1.035885,-0.915653 +100523534.720,100.523535,-0.935524,-0.933751 +100523827.688,100.523828,-0.650724,-0.953816 +100524120.657,100.524121,-0.712160,-0.974772 +100524413.625,100.524414,-0.968452,-0.991132 +100524706.594,100.524707,-1.435380,-1.009237 +100524999.562,100.525000,-1.258053,-1.031168 +100525292.531,100.525293,-1.087085,-1.059412 +100525585.500,100.525585,-0.917421,-1.083697 +100525878.468,100.525878,-0.899357,-1.103155 +100526171.437,100.526171,-1.052937,-1.126653 +100526464.405,100.526464,-1.693923,-1.151763 +100526757.374,100.526757,-1.454457,-1.173635 +100527050.342,100.527050,-1.384797,-1.196160 +100527343.311,100.527343,-1.008261,-1.223107 +100527636.279,100.527636,-0.666452,-1.246992 +100527929.248,100.527929,-1.049186,-1.273593 +100528222.216,100.528222,-1.507626,-1.297998 +100528515.185,100.528515,-1.177063,-1.325712 +100528808.153,100.528808,-0.946926,-1.353989 +100529101.122,100.529101,-1.014241,-1.383740 +100529394.090,100.529394,-0.755569,-1.414603 +100529687.059,100.529687,-0.798871,-1.447251 +100529980.027,100.529980,-1.045584,-1.484913 +100530272.996,100.530273,-1.240984,-1.517340 +100530565.964,100.530566,-1.569935,-1.545059 +100530858.933,100.530859,-1.422982,-1.570818 +100531151.901,100.531152,-1.810428,-1.595523 +100531444.870,100.531445,-1.140883,-1.617154 +100531737.838,100.531738,-0.947046,-1.645682 +100532030.807,100.532031,-1.260670,-1.677216 +100532323.775,100.532324,-1.897243,-1.704357 +100532616.744,100.532617,-1.021959,-1.728893 +100532909.712,100.532910,-1.093565,-1.756485 +100533202.681,100.533203,-1.748967,-1.782437 +100533495.649,100.533496,-1.596847,-1.810204 +100533788.618,100.533789,-1.476426,-1.840523 +100534081.586,100.534082,-1.721187,-1.873841 +100534374.555,100.534375,-1.584345,-1.907806 +100534667.523,100.534668,-1.610385,-1.936461 +100534960.492,100.534960,-1.477720,-1.965188 +100535253.460,100.535253,-1.458100,-1.995143 +100535546.429,100.535546,-1.786771,-2.022386 +100535839.397,100.535839,-2.225254,-2.047065 +100536132.366,100.536132,-2.140549,-2.073481 +100536425.334,100.536425,-2.398215,-2.099593 +100536718.303,100.536718,-2.325654,-2.128586 +100537011.271,100.537011,-2.215263,-2.154572 +100537304.240,100.537304,-2.622073,-2.184846 +100537597.208,100.537597,-2.774353,-2.214286 +100537890.177,100.537890,-2.207086,-2.239469 +100538183.145,100.538183,-2.304581,-2.264983 +100538476.114,100.538476,-2.289385,-2.297144 +100538769.082,100.538769,-2.867890,-2.332276 +100539062.051,100.539062,-2.805791,-2.361811 +100539355.019,100.539355,-2.637199,-2.392103 +100539647.988,100.539648,-2.613960,-2.423778 +100539940.956,100.539941,-2.888271,-2.453582 +100540233.925,100.540234,-2.661149,-2.485966 +100540526.893,100.540527,-2.184389,-2.521018 +100540819.862,100.540820,-2.251930,-2.551847 +100541112.830,100.541113,-2.380144,-2.581181 +100541405.799,100.541406,-2.309999,-2.615142 +100541698.767,100.541699,-2.470958,-2.654079 +100541991.736,100.541992,-3.343874,-2.693931 +100542284.704,100.542285,-3.620136,-2.733442 +100542577.673,100.542578,-2.773272,-2.777091 +100542870.641,100.542871,-2.559526,-2.813917 +100543163.610,100.543164,-2.837342,-2.852369 +100543456.578,100.543457,-2.837414,-2.888868 +100543749.547,100.543750,-2.518456,-2.926614 +100544042.515,100.544043,-2.493309,-2.969147 +100544335.484,100.544335,-2.314144,-3.009406 +100544628.452,100.544628,-2.846784,-3.047220 +100544921.421,100.544921,-3.694698,-3.080782 +100545214.389,100.545214,-3.975828,-3.116485 +100545507.358,100.545507,-3.825683,-3.154057 +100545800.326,100.545800,-3.548072,-3.196672 +100546093.295,100.546093,-3.612243,-3.241221 +100546386.263,100.546386,-3.933927,-3.283530 +100546679.232,100.546679,-3.449201,-3.319681 +100546972.200,100.546972,-3.650719,-3.359251 +100547265.169,100.547265,-3.992236,-3.399692 +100547558.137,100.547558,-3.732429,-3.444750 +100547851.106,100.547851,-3.607544,-3.495881 +100548144.074,100.548144,-3.586049,-3.542483 +100548437.043,100.548437,-4.335899,-3.584891 +100548730.011,100.548730,-4.196603,-3.631594 +100549022.980,100.549023,-3.832706,-3.682062 +100549315.948,100.549316,-3.661482,-3.733761 +100549608.917,100.549609,-3.684698,-3.790179 +100549901.885,100.549902,-3.971221,-3.847512 +100550194.854,100.550195,-4.120519,-3.901371 +100550487.823,100.550488,-4.142565,-3.953016 +100550780.791,100.550781,-4.346211,-4.001398 +100551073.760,100.551074,-3.876328,-4.052437 +100551366.728,100.551367,-2.816026,-4.099399 +100551659.697,100.551660,-3.502772,-4.155952 +100551952.665,100.551953,-4.035285,-4.216437 +100552245.634,100.552246,-3.531871,-4.273342 +100552538.602,100.552539,-4.068956,-4.323339 +100552831.571,100.552832,-4.449709,-4.387589 +100553124.539,100.553125,-4.081578,-4.452077 +100553417.508,100.553418,-3.978596,-4.507559 +100553710.476,100.553710,-4.477391,-4.566520 +100554003.445,100.554003,-4.309604,-4.628795 +100554296.413,100.554296,-4.316429,-4.687728 +100554589.382,100.554589,-4.600489,-4.749319 +100554882.350,100.554882,-4.621212,-4.812030 +100555175.319,100.555175,-4.409216,-4.878077 +100555468.287,100.555468,-4.492687,-4.945051 +100555761.256,100.555761,-4.836376,-5.008222 +100556054.224,100.556054,-4.909823,-5.066112 +100556347.193,100.556347,-4.698002,-5.125796 +100556640.161,100.556640,-4.920883,-5.183092 +100556933.130,100.556933,-5.396070,-5.242326 +100557226.098,100.557226,-5.462275,-5.303825 +100557519.067,100.557519,-5.215506,-5.360391 +100557812.035,100.557812,-5.118029,-5.415736 +100558105.004,100.558105,-5.869782,-5.480229 +100558397.972,100.558398,-6.010843,-5.544344 +100558690.941,100.558691,-5.763904,-5.609470 +100558983.909,100.558984,-5.678281,-5.667302 +100559276.878,100.559277,-6.470509,-5.726841 +100559569.846,100.559570,-7.272827,-5.789676 +100559862.815,100.559863,-7.155227,-5.853935 +100560155.783,100.560156,-6.921782,-5.915250 +100560448.752,100.560449,-7.548298,-5.981230 +100560741.720,100.560742,-6.659137,-6.056387 +100561034.689,100.561035,-6.452164,-6.131597 +100561327.657,100.561328,-6.520373,-6.205649 +100561620.626,100.561621,-7.211083,-6.282458 +100561913.594,100.561914,-6.646678,-6.357873 +100562206.563,100.562207,-6.261367,-6.420467 +100562499.531,100.562500,-6.571764,-6.479947 +100562792.500,100.562792,-6.223340,-6.553773 +100563085.468,100.563085,-6.950795,-6.632637 +100563378.437,100.563378,-7.137970,-6.707974 +100563671.405,100.563671,-7.063620,-6.784622 +100563964.374,100.563964,-7.440372,-6.868786 +100564257.342,100.564257,-6.763831,-6.955112 +100564550.311,100.564550,-6.858224,-7.047149 +100564843.279,100.564843,-7.083858,-7.130089 +100565136.248,100.565136,-7.061992,-7.199273 +100565429.216,100.565429,-6.930034,-7.265158 +100565722.185,100.565722,-7.891832,-7.334501 +100566015.153,100.566015,-8.439235,-7.409915 +100566308.122,100.566308,-8.096593,-7.485526 +100566601.090,100.566601,-8.421899,-7.556949 +100566894.059,100.566894,-8.637635,-7.638188 +100567187.027,100.567187,-8.504945,-7.717428 +100567479.996,100.567480,-8.343627,-7.791993 +100567772.964,100.567773,-8.783856,-7.872450 +100568065.933,100.568066,-8.807118,-7.956941 +100568358.901,100.568359,-9.287206,-8.043866 +100568651.870,100.568652,-9.182574,-8.118235 +100568944.838,100.568945,-9.274455,-8.196398 +100569237.807,100.569238,-10.119260,-8.283085 +100569530.775,100.569531,-10.368494,-8.375162 +100569823.744,100.569824,-10.899318,-8.468843 +100570116.712,100.570117,-10.466691,-8.558469 +100570409.681,100.570410,-10.366743,-8.646812 +100570702.649,100.570703,-10.663630,-8.736455 +100570995.618,100.570996,-10.777827,-8.823744 +100571288.586,100.571289,-10.993475,-8.923211 +100571581.555,100.571582,-11.006204,-9.053774 +100571874.523,100.571875,-11.171497,-9.168378 +100572167.492,100.572167,-11.391431,-9.272328 +100572460.460,100.572460,-12.228944,-9.392163 +100572753.429,100.572753,-13.293073,-9.500882 +100573046.397,100.573046,-13.751825,-9.602878 +100573339.366,100.573339,-13.505335,-9.716759 +100573632.334,100.573632,-14.203059,-9.836564 +100573925.303,100.573925,-15.189871,-9.946164 +100574218.271,100.574218,-14.345046,-10.063077 +100574511.240,100.574511,-14.114047,-10.183039 +100574804.208,100.574804,-14.921999,-10.298444 +100575097.177,100.575097,-15.407750,-10.416448 +100575390.146,100.575390,-14.820093,-10.543994 +100575683.114,100.575683,-15.355956,-10.672886 +100575976.083,100.575976,-15.767359,-10.795465 +100576269.051,100.576269,-15.453459,-10.919385 +100576562.020,100.576562,-16.181988,-11.053444 +100576854.988,100.576855,-16.434807,-11.184749 +100577147.957,100.577148,-16.137011,-11.305866 +100577440.925,100.577441,-16.194042,-11.428432 +100577733.894,100.577734,-16.382004,-11.562139 +100578026.862,100.578027,-16.720350,-11.703405 +100578319.831,100.578320,-17.033987,-11.825694 +100578612.799,100.578613,-16.690598,-11.947400 +100578905.768,100.578906,-17.300179,-12.080140 +100579198.736,100.579199,-17.389362,-12.219872 +100579491.705,100.579492,-17.339087,-12.339690 +100579784.673,100.579785,-17.626387,-12.441665 +100580077.642,100.580078,-18.150183,-12.548998 +100580370.610,100.580371,-18.938499,-12.665390 +100580663.579,100.580664,-19.295635,-12.768566 +100580956.547,100.580957,-19.139078,-12.898930 +100581249.516,100.581250,-18.651537,-13.040136 +100581542.484,100.581542,-19.164200,-13.183799 +100581835.453,100.581835,-19.587658,-13.310116 +100582128.421,100.582128,-20.195974,-13.458720 +100582421.390,100.582421,-20.181570,-13.627135 +100582714.358,100.582714,-20.608500,-13.789800 +100583007.327,100.583007,-20.198637,-13.973431 +100583300.295,100.583300,-20.591003,-14.134871 +100583593.264,100.583593,-20.531054,-14.295367 +100583886.232,100.583886,-20.636026,-14.464783 +100584179.201,100.584179,-20.592142,-14.625935 +100584472.169,100.584472,-21.091541,-14.822745 +100584765.138,100.584765,-21.143488,-15.024320 +100585058.106,100.585058,-21.055912,-15.224626 +100585351.075,100.585351,-21.409357,-15.435782 +100585644.043,100.585644,-21.505072,-15.664750 +100585937.012,100.585937,-22.162401,-15.856918 +100586229.980,100.586230,-22.222767,-16.033123 +100586522.949,100.586523,-22.597559,-16.232565 +100586815.917,100.586816,-21.979841,-16.426184 +100587108.886,100.587109,-23.222904,-16.618840 +100587401.854,100.587402,-23.609135,-16.826672 +100587694.823,100.587695,-22.753275,-17.053606 +100587987.791,100.587988,-23.055573,-17.269217 +100588280.760,100.588281,-24.731560,-17.494813 +100588573.728,100.588574,-24.519258,-17.707044 +100588866.697,100.588867,-24.440174,-17.936022 +100589159.665,100.589160,-24.426922,-18.172225 +100589452.634,100.589453,-24.412970,-18.376106 +100589745.602,100.589746,-25.113270,-18.577246 +100590038.571,100.590039,-25.790035,-18.762918 +100590331.539,100.590332,-25.611128,-18.978446 +100590624.508,100.590625,-25.159687,-19.210906 +100590917.476,100.590917,-25.321497,-19.439836 +100591210.445,100.591210,-25.367296,-19.674528 +100591503.413,100.591503,-25.274929,-19.910324 +100591796.382,100.591796,-25.608988,-20.158711 +100592089.350,100.592089,-26.056320,-20.411327 +100592382.319,100.592382,-25.933016,-20.666074 +100592675.287,100.592675,-26.477291,-20.887734 +100592968.256,100.592968,-26.546492,-21.068107 +100593261.224,100.593261,-26.622631,-21.236591 +100593554.193,100.593554,-26.605579,-21.422801 +100593847.161,100.593847,-26.649851,-21.586579 +100594140.130,100.594140,-26.403374,-21.719319 +100594433.098,100.594433,-26.445662,-21.887483 +100594726.067,100.594726,-26.438837,-22.073895 +100595019.035,100.595019,-26.582493,-22.234463 +100595312.004,100.595312,-26.848644,-22.382071 +100595604.972,100.595605,-27.087759,-22.558092 +100595897.941,100.595898,-27.399464,-22.718986 +100596190.909,100.596191,-27.040796,-22.869782 +100596483.878,100.596484,-26.949963,-23.039223 +100596776.846,100.596777,-27.182468,-23.187151 +100597069.815,100.597070,-27.366810,-23.330004 +100597362.783,100.597363,-26.849396,-23.488097 +100597655.752,100.597656,-27.030725,-23.649191 +100597948.720,100.597949,-27.559814,-23.809695 +100598241.689,100.598242,-27.296413,-23.963477 +100598534.657,100.598535,-27.209482,-24.111159 +100598827.626,100.598828,-27.341028,-24.276108 +100599120.594,100.599121,-27.414072,-24.422240 +100599413.563,100.599414,-27.481403,-24.571401 +100599706.531,100.599707,-27.291121,-24.728855 +100599999.500,100.599999,-27.189814,-24.880501 +100600292.469,100.600292,-27.670374,-25.018393 +100600585.437,100.600585,-27.571062,-25.134512 +100600878.406,100.600878,-27.803490,-25.240359 +100601171.374,100.601171,-28.176790,-25.354154 +100601464.343,100.601464,-28.352278,-25.487864 +100601757.311,100.601757,-28.588076,-25.609018 +100602050.280,100.602050,-28.590519,-25.721175 +100602343.248,100.602343,-28.163904,-25.819737 +100602636.217,100.602636,-27.633278,-25.919953 +100602929.185,100.602929,-27.969473,-26.011915 +100603222.154,100.603222,-28.080473,-26.116418 +100603515.122,100.603515,-28.359125,-26.207987 +100603808.091,100.603808,-27.746199,-26.304392 +100604101.059,100.604101,-27.502768,-26.400973 +100604394.028,100.604394,-28.343863,-26.501907 +100604686.996,100.604687,-28.312168,-26.590245 +100604979.965,100.604980,-28.172619,-26.678596 +100605272.933,100.605273,-28.351725,-26.772622 +100605565.902,100.605566,-28.190025,-26.858929 +100605858.870,100.605859,-28.008062,-26.945740 +100606151.839,100.606152,-27.367895,-27.017873 +100606444.807,100.606445,-27.656576,-27.088993 +100606737.776,100.606738,-27.985338,-27.154658 +100607030.744,100.607031,-28.288736,-27.236058 +100607323.713,100.607324,-27.839144,-27.292976 +100607616.681,100.607617,-27.963917,-27.340304 +100607909.650,100.607910,-28.109192,-27.406543 +100608202.618,100.608203,-28.072386,-27.470832 +100608495.587,100.608496,-28.223394,-27.509460 +100608788.555,100.608789,-28.497135,-27.548616 +100609081.524,100.609082,-28.128731,-27.585674 +100609374.492,100.609374,-27.996725,-27.621853 +100609667.461,100.609667,-27.573730,-27.660158 +100609960.429,100.609960,-27.610897,-27.689822 +100610253.398,100.610253,-27.899860,-27.710118 +100610546.366,100.610546,-28.056059,-27.734041 +100610839.335,100.610839,-28.337072,-27.765186 +100611132.303,100.611132,-27.930302,-27.792456 +100611425.272,100.611425,-28.186321,-27.821516 +100611718.240,100.611718,-28.028900,-27.851460 +100612011.209,100.612011,-27.645052,-27.875446 +100612304.177,100.612304,-27.985878,-27.894280 +100612597.146,100.612597,-28.603674,-27.915999 +100612890.114,100.612890,-28.484077,-27.934016 +100613183.083,100.613183,-28.579039,-27.951674 +100613476.051,100.613476,-28.659019,-27.968290 +100613769.020,100.613769,-28.374552,-27.986307 +100614061.988,100.614062,-27.823355,-28.002620 +100614354.957,100.614355,-27.577440,-28.020378 +100614647.925,100.614648,-28.217300,-28.030621 +100614940.894,100.614941,-28.697412,-28.045278 +100615233.862,100.615234,-28.474031,-28.061768 +100615526.831,100.615527,-28.189068,-28.073106 +100615819.799,100.615820,-28.275997,-28.081881 +100616112.768,100.616113,-28.230608,-28.090142 +100616405.736,100.616406,-28.493483,-28.102535 +100616698.705,100.616699,-28.857471,-28.109254 +100616991.673,100.616992,-28.400852,-28.117951 +100617284.642,100.617285,-27.987463,-28.126352 +100617577.610,100.617578,-27.756046,-28.138141 +100617870.579,100.617871,-28.096632,-28.148614 +100618163.547,100.618164,-28.371119,-28.152433 +100618456.516,100.618457,-28.513258,-28.161896 +100618749.484,100.618749,-27.923475,-28.170348 +100619042.453,100.619042,-27.733650,-28.179909 +100619335.421,100.619335,-28.143469,-28.189592 +100619628.390,100.619628,-28.451303,-28.198206 +100619921.358,100.619921,-28.390642,-28.204664 +100620214.327,100.620214,-28.559980,-28.209608 +100620507.295,100.620507,-28.579042,-28.212811 +100620800.264,100.620800,-27.830954,-28.217589 +100621093.232,100.621093,-27.981974,-28.222209 +100621386.201,100.621386,-28.140230,-28.224611 +100621679.169,100.621679,-28.327276,-28.229344 +100621972.138,100.621972,-28.550789,-28.230967 +100622265.106,100.622265,-28.778343,-28.229965 +100622558.075,100.622558,-28.514620,-28.233137 +100622851.043,100.622851,-28.702721,-28.238010 +100623144.012,100.623144,-28.558819,-28.232604 +100623436.980,100.623437,-27.600155,-28.227237 +100623729.949,100.623730,-27.831701,-28.226523 +100624022.917,100.624023,-28.009216,-28.227782 +100624315.886,100.624316,-28.190269,-28.233895 +100624608.854,100.624609,-27.945818,-28.233185 +100624901.823,100.624902,-27.863007,-28.231980 +100625194.792,100.625195,-28.131897,-28.229372 +100625487.760,100.625488,-28.066025,-28.228574 +100625780.729,100.625781,-28.289030,-28.230490 +100626073.697,100.626074,-28.185291,-28.234780 +100626366.666,100.626367,-28.034304,-28.243925 +100626659.634,100.626660,-28.298212,-28.252002 +100626952.603,100.626953,-28.413994,-28.258369 +100627245.571,100.627246,-28.568901,-28.256871 +100627538.540,100.627539,-27.995401,-28.256608 +100627831.508,100.627832,-28.299927,-28.254772 +100628124.477,100.628124,-28.892698,-28.252456 +100628417.445,100.628417,-29.236925,-28.251426 +100628710.414,100.628710,-28.694805,-28.228744 +100629003.382,100.629003,-28.095598,-28.216257 +100629296.351,100.629296,-27.886835,-28.219616 +100629589.319,100.629589,-28.125391,-28.223260 +100629882.288,100.629882,-28.289961,-28.229450 +100630175.256,100.630175,-28.099327,-28.233596 +100630468.225,100.630468,-28.320736,-28.237409 +100630761.193,100.630761,-28.543114,-28.240654 +100631054.162,100.631054,-28.233284,-28.241982 +100631347.130,100.631347,-28.579634,-28.240598 +100631640.099,100.631640,-28.496088,-28.241717 +100631933.067,100.631933,-28.207281,-28.246549 +100632226.036,100.632226,-28.202833,-28.254886 +100632519.004,100.632519,-28.413069,-28.257035 +100632811.973,100.632812,-28.748556,-28.255505 +100633104.941,100.633105,-28.793030,-28.250587 +100633397.910,100.633398,-28.735542,-28.249290 +100633690.878,100.633691,-28.929855,-28.249355 +100633983.847,100.633984,-28.708910,-28.248649 +100634276.815,100.634277,-28.527557,-28.247398 +100634569.784,100.634570,-27.550335,-28.255014 +100634862.752,100.634863,-28.113333,-28.255651 +100635155.721,100.635156,-28.594156,-28.249859 +100635448.689,100.635449,-28.216219,-28.248864 +100635741.658,100.635742,-28.169109,-28.249278 +100636034.626,100.636035,-28.492472,-28.245582 +100636327.595,100.636328,-28.630766,-28.244160 +100636620.563,100.636621,-27.719709,-28.242558 +100636913.532,100.636914,-28.269480,-28.237577 +100637206.500,100.637207,-28.460894,-28.234055 +100637499.469,100.637499,-28.256657,-28.234837 +100637792.437,100.637792,-28.313330,-28.239918 +100638085.406,100.638085,-28.042257,-28.241972 +100638378.374,100.638378,-28.516088,-28.240527 +100638671.343,100.638671,-28.254738,-28.241781 +100638964.311,100.638964,-28.584150,-28.246282 +100639257.280,100.639257,-28.697094,-28.252684 +100639550.248,100.639550,-28.619917,-28.253574 +100639843.217,100.639843,-28.075256,-28.253164 +100640136.185,100.640136,-27.761730,-28.253778 +100640429.154,100.640429,-28.076284,-28.256675 +100640722.122,100.640722,-28.175512,-28.256100 +100641015.091,100.641015,-28.420467,-28.255043 +100641308.059,100.641308,-28.515167,-28.253630 +100641601.028,100.641601,-29.076828,-28.253672 +100641893.996,100.641894,-28.836519,-28.254152 +100642186.965,100.642187,-28.443914,-28.252212 +100642479.933,100.642480,-28.614305,-28.253002 +100642772.902,100.642773,-28.257189,-28.257821 +100643065.870,100.643066,-27.323353,-28.257624 +100643358.839,100.643359,-27.422583,-28.259947 +100643651.807,100.643652,-28.259428,-28.270213 +100643944.776,100.643945,-27.903315,-28.274516 +100644237.744,100.644238,-28.275946,-28.276104 +100644530.713,100.644531,-28.245199,-28.276703 +100644823.681,100.644824,-28.147211,-28.281275 +100645116.650,100.645117,-27.833729,-28.288093 +100645409.618,100.645410,-28.240635,-28.292535 +100645702.587,100.645703,-28.458187,-28.295009 +100645995.555,100.645996,-28.605471,-28.294847 +100646288.524,100.646289,-28.537218,-28.296298 +100646581.492,100.646581,-28.748764,-28.295891 +100646874.461,100.646874,-28.893713,-28.291783 +100647167.429,100.647167,-28.086828,-28.288282 +100647460.398,100.647460,-27.806551,-28.285351 +100647753.366,100.647753,-27.734856,-28.288582 +100648046.335,100.648046,-27.812620,-28.290455 +100648339.303,100.648339,-27.939058,-28.287864 +100648632.272,100.648632,-25.886400,-28.282542 +100648925.240,100.648925,-26.975470,-28.280452 +100649218.209,100.649218,-28.604628,-28.280707 +100649511.177,100.649511,-28.498674,-28.284816 +100649804.146,100.649804,-28.372660,-28.290608 +100650097.115,100.650097,-28.133442,-28.291890 +100650390.083,100.650390,-28.412596,-28.291890 +100650683.052,100.650683,-28.504633,-28.293521 +100650976.020,100.650976,-28.527182,-28.291554 +100651268.989,100.651269,-27.757320,-28.290169 +100651561.957,100.651562,-28.340351,-28.286819 +100651854.926,100.651855,-28.709480,-28.284271 +100652147.894,100.652148,-28.773184,-28.286733 +100652440.863,100.652441,-28.271812,-28.285250 +100652733.831,100.652734,-28.382343,-28.281845 +100653026.800,100.653027,-27.825918,-28.276585 +100653319.768,100.653320,-28.391518,-28.274367 +100653612.737,100.653613,-28.668737,-28.268812 +100653905.705,100.653906,-28.276140,-28.262694 +100654198.674,100.654199,-27.670607,-28.261149 +100654491.642,100.654492,-28.577822,-28.255977 +100654784.611,100.654785,-28.304705,-28.257767 +100655077.579,100.655078,-27.896786,-28.254189 +100655370.548,100.655371,-28.332905,-28.246489 +100655663.516,100.655664,-28.245371,-28.246427 +100655956.485,100.655956,-27.794003,-28.246696 +100656249.453,100.656249,-28.040726,-28.242435 +100656542.422,100.656542,-28.266989,-28.238181 +100656835.390,100.656835,-28.134420,-28.241466 +100657128.359,100.657128,-27.926573,-28.235704 +100657421.327,100.657421,-28.089884,-28.232684 +100657714.296,100.657714,-28.428629,-28.232920 +100658007.264,100.658007,-28.377666,-28.227803 +100658300.233,100.658300,-28.171818,-28.228013 +100658593.201,100.658593,-28.700172,-28.224407 +100658886.170,100.658886,-28.537628,-28.223316 +100659179.138,100.659179,-28.592056,-28.220553 +100659472.107,100.659472,-28.263990,-28.212566 +100659765.075,100.659765,-28.392946,-28.205696 +100660058.044,100.660058,-28.478289,-28.207645 +100660351.012,100.660351,-29.008097,-28.213917 +100660643.981,100.660644,-28.494911,-28.217295 +100660936.949,100.660937,-27.701597,-28.213066 +100661229.918,100.661230,-27.803949,-28.206278 +100661522.886,100.661523,-28.145761,-28.202869 +100661815.855,100.661816,-28.394674,-28.195125 +100662108.823,100.662109,-28.275167,-28.185232 +100662401.792,100.662402,-28.902206,-28.180471 +100662694.760,100.662695,-29.278894,-28.177837 +100662987.729,100.662988,-28.672930,-28.177072 +100663280.697,100.663281,-28.914017,-28.185559 +100663573.666,100.663574,-29.013996,-28.190313 +100663866.634,100.663867,-28.399948,-28.187224 +100664159.603,100.664160,-28.218765,-28.189172 +100664452.571,100.664453,-28.271496,-28.186780 +100664745.540,100.664746,-28.568426,-28.184636 +100665038.508,100.665039,-28.806131,-28.184161 +100665331.477,100.665331,-28.762274,-28.188862 +100665624.445,100.665624,-28.400129,-28.189330 +100665917.414,100.665917,-28.266886,-28.184085 +100666210.382,100.666210,-28.383528,-28.180496 +100666503.351,100.666503,-27.982164,-28.179744 +100666796.319,100.666796,-27.768503,-28.174715 +100667089.288,100.667089,-27.946569,-28.170280 +100667382.256,100.667382,-28.160246,-28.171242 +100667675.225,100.667675,-28.430346,-28.175676 +100667968.193,100.667968,-28.565067,-28.180654 +100668261.162,100.668261,-28.502878,-28.183969 +100668554.130,100.668554,-28.412590,-28.187794 +100668847.099,100.668847,-28.390793,-28.209414 +100669140.067,100.669140,-28.129196,-28.214501 +100669433.036,100.669433,-28.433744,-28.207448 +100669726.004,100.669726,-28.966124,-28.205069 +100670018.973,100.670019,-28.469238,-28.203343 +100670311.941,100.670312,-28.099283,-28.204907 +100670604.910,100.670605,-28.551645,-28.205900 +100670897.878,100.670898,-28.266676,-28.208936 +100671190.847,100.671191,-28.050001,-28.210195 +100671483.815,100.671484,-28.114698,-28.214260 +100671776.784,100.671777,-28.144224,-28.214959 +100672069.752,100.672070,-28.551863,-28.217183 +100672362.721,100.672363,-28.007952,-28.217422 +100672655.689,100.672656,-27.957285,-28.218323 +100672948.658,100.672949,-28.011908,-28.217993 +100673241.626,100.673242,-28.463434,-28.220492 +100673534.595,100.673535,-27.961882,-28.217485 +100673827.563,100.673828,-28.048971,-28.215589 +100674120.532,100.674121,-28.480473,-28.219418 +100674413.500,100.674414,-27.832331,-28.222478 +100674706.469,100.674706,-27.763968,-28.217423 +100674999.438,100.674999,-27.663116,-28.218399 +100675292.406,100.675292,-27.580503,-28.223029 +100675585.375,100.675585,-28.207684,-28.226928 +100675878.343,100.675878,-28.205452,-28.232459 +100676171.312,100.676171,-27.914680,-28.240093 +100676464.280,100.676464,-28.035818,-28.242613 +100676757.249,100.676757,-28.138506,-28.242291 +100677050.217,100.677050,-27.539448,-28.244264 +100677343.186,100.677343,-28.045488,-28.249232 +100677636.154,100.677636,-28.289209,-28.252042 +100677929.123,100.677929,-27.651836,-28.249083 +100678222.091,100.678222,-28.069895,-28.253048 +100678515.060,100.678515,-28.017799,-28.258867 +100678808.028,100.678808,-28.106747,-28.258832 +100679100.997,100.679101,-28.190989,-28.259894 +100679393.965,100.679394,-27.620142,-28.258394 +100679686.934,100.679687,-27.691919,-28.255932 +100679979.902,100.679980,-28.342432,-28.255761 +100680272.871,100.680273,-28.615225,-28.253906 +100680565.839,100.680566,-28.549253,-28.249207 +100680858.808,100.680859,-27.636299,-28.249709 +100681151.776,100.681152,-27.539915,-28.257282 +100681444.745,100.681445,-28.040421,-28.263728 +100681737.713,100.681738,-27.940735,-28.266320 +100682030.682,100.682031,-27.494223,-28.268374 +100682323.650,100.682324,-27.801226,-28.268130 +100682616.619,100.682617,-28.232649,-28.264579 +100682909.587,100.682910,-28.151804,-28.259216 +100683202.556,100.683203,-28.400806,-28.259957 +100683495.524,100.683496,-28.006197,-28.255892 +100683788.493,100.683788,-27.849260,-28.253327 +100684081.461,100.684081,-28.160793,-28.256097 +100684374.430,100.684374,-27.953791,-28.257329 +100684667.398,100.684667,-27.957109,-28.253073 +100684960.367,100.684960,-28.083147,-28.249938 +100685253.335,100.685253,-28.472273,-28.246516 +100685546.304,100.685546,-28.306036,-28.243918 +100685839.272,100.685839,-27.753656,-28.243565 +100686132.241,100.686132,-28.094721,-28.243881 +100686425.209,100.686425,-28.426718,-28.245175 +100686718.178,100.686718,-28.026499,-28.249266 +100687011.146,100.687011,-28.229809,-28.257961 +100687304.115,100.687304,-28.217957,-28.263940 +100687597.083,100.687597,-28.404102,-28.265612 +100687890.052,100.687890,-28.399008,-28.263249 +100688183.020,100.688183,-28.251114,-28.261239 +100688475.989,100.688476,-28.464647,-28.261433 +100688768.957,100.688769,-28.108294,-28.264295 +100689061.926,100.689062,-27.534203,-28.264295 +100689354.894,100.689355,-27.657843,-28.267652 +100689647.863,100.689648,-28.163244,-28.264085 +100689940.831,100.689941,-28.133564,-28.254268 +100690233.800,100.690234,-28.349516,-28.249078 +100690526.768,100.690527,-28.557720,-28.250115 +100690819.737,100.690820,-28.974600,-28.250046 +100691112.705,100.691113,-28.716831,-28.252426 +100691405.674,100.691406,-28.290178,-28.251613 +100691698.642,100.691699,-28.440144,-28.251705 +100691991.611,100.691992,-29.064932,-28.254322 +100692284.579,100.692285,-28.810591,-28.255550 +100692577.548,100.692578,-28.398615,-28.260409 +100692870.516,100.692871,-28.335638,-28.266369 +100693163.485,100.693163,-28.150520,-28.270892 +100693456.453,100.693456,-27.982737,-28.272369 +100693749.422,100.693749,-28.389587,-28.275644 +100694042.390,100.694042,-28.843241,-28.276413 +100694335.359,100.694335,-28.056589,-28.274816 +100694628.327,100.694628,-27.884354,-28.280077 +100694921.296,100.694921,-28.443214,-28.284662 +100695214.264,100.695214,-28.529427,-28.287274 +100695507.233,100.695507,-28.918364,-28.292028 +100695800.201,100.695800,-29.081249,-28.294289 +100696093.170,100.696093,-28.859282,-28.300109 +100696386.138,100.696386,-28.383520,-28.308296 +100696679.107,100.696679,-28.222885,-28.313438 +100696972.075,100.696972,-28.406281,-28.314969 +100697265.044,100.697265,-28.609869,-28.322408 +100697558.012,100.697558,-28.477739,-28.328014 +100697850.981,100.697851,-28.025591,-28.332815 +100698143.949,100.698144,-28.976347,-28.340610 +100698436.918,100.698437,-29.033800,-28.345013 +100698729.886,100.698730,-28.694876,-28.349805 +100699022.855,100.699023,-28.695677,-28.355158 +100699315.823,100.699316,-28.375589,-28.356389 +100699608.792,100.699609,-27.938684,-28.364024 +100699901.760,100.699902,-28.368847,-28.368666 +100700194.729,100.700195,-28.218683,-28.370507 +100700487.698,100.700488,-28.302362,-28.370983 +100700780.666,100.700781,-28.568254,-28.370153 +100701073.635,100.701074,-28.727556,-28.376866 +100701366.603,100.701367,-28.681894,-28.382414 +100701659.572,100.701660,-28.505833,-28.385421 +100701952.540,100.701953,-28.694489,-28.391866 +100702245.509,100.702246,-28.241804,-28.400251 +100702538.477,100.702538,-28.372763,-28.403656 +100702831.446,100.702831,-28.437315,-28.407794 +100703124.414,100.703124,-28.786043,-28.412811 +100703417.383,100.703417,-28.310278,-28.415268 +100703710.351,100.703710,-28.614407,-28.419087 +100704003.320,100.704003,-28.811232,-28.425390 +100704296.288,100.704296,-28.389408,-28.431498 +100704589.257,100.704589,-27.722399,-28.435786 +100704882.225,100.704882,-28.129385,-28.439904 +100705175.194,100.705175,-28.303377,-28.437346 +100705468.162,100.705468,-28.378681,-28.437160 +100705761.131,100.705761,-28.350332,-28.441288 +100706054.099,100.706054,-28.310633,-28.446830 +100706347.068,100.706347,-28.570492,-28.447105 +100706640.036,100.706640,-28.544310,-28.445994 +100706933.005,100.706933,-28.990892,-28.448150 +100707225.973,100.707226,-28.784304,-28.450617 +100707518.942,100.707519,-28.389853,-28.454682 +100707811.910,100.707812,-28.106340,-28.460187 +100708104.879,100.708105,-28.279421,-28.463093 +100708397.847,100.708398,-28.531027,-28.462763 +100708690.816,100.708691,-28.838720,-28.459869 +100708983.784,100.708984,-28.390762,-28.462738 +100709276.753,100.709277,-28.599438,-28.472113 +100709569.721,100.709570,-27.953350,-28.481216 +100709862.690,100.709863,-27.616646,-28.484894 +100710155.658,100.710156,-27.779522,-28.487214 +100710448.627,100.710449,-28.238621,-28.486660 +100710741.595,100.710742,-28.541655,-28.483914 +100711034.564,100.711035,-28.606819,-28.480853 +100711327.532,100.711328,-27.944979,-28.480823 +100711620.501,100.711621,-28.126909,-28.480841 +100711913.469,100.711913,-28.508789,-28.481999 +100712206.438,100.712206,-28.735771,-28.481388 +100712499.406,100.712499,-28.686867,-28.480688 +100712792.375,100.712792,-28.793894,-28.480653 +100713085.343,100.713085,-28.639145,-28.484767 +100713378.312,100.713378,-28.680241,-28.485454 +100713671.280,100.713671,-28.400940,-28.485717 +100713964.249,100.713964,-28.150061,-28.485431 +100714257.217,100.714257,-28.257038,-28.480730 +100714550.186,100.714550,-28.537102,-28.479828 +100714843.154,100.714843,-28.360561,-28.479296 +100715136.123,100.715136,-27.984774,-28.476636 +100715429.091,100.715429,-28.172155,-28.476215 +100715722.060,100.715722,-28.522655,-28.469962 +100716015.028,100.716015,-29.065935,-28.465570 +100716307.997,100.716308,-29.084444,-28.464004 +100716600.965,100.716601,-28.753092,-28.465597 +100716893.934,100.716894,-28.344751,-28.469672 +100717186.902,100.717187,-28.487514,-28.472765 +100717479.871,100.717480,-28.832930,-28.471986 +100717772.839,100.717773,-28.995024,-28.472744 +100718065.808,100.718066,-28.675730,-28.475993 +100718358.776,100.718359,-28.676840,-28.470518 +100718651.745,100.718652,-28.673471,-28.466222 +100718944.713,100.718945,-28.861696,-28.464716 +100719237.682,100.719238,-28.356497,-28.460146 +100719530.650,100.719531,-28.606190,-28.462939 +100719823.619,100.719824,-28.273630,-28.467909 +100720116.587,100.720117,-28.600637,-28.469221 +100720409.556,100.720410,-28.684740,-28.469599 +100720702.524,100.720703,-28.432331,-28.468749 +100720995.493,100.720995,-28.491306,-28.467990 +100721288.461,100.721288,-28.216478,-28.465701 +100721581.430,100.721581,-28.438759,-28.464807 +100721874.398,100.721874,-28.820555,-28.464325 +100722167.367,100.722167,-28.545029,-28.464263 +100722460.335,100.722460,-28.227734,-28.464624 +100722753.304,100.722753,-28.814766,-28.465568 +100723046.272,100.723046,-28.853058,-28.465735 +100723339.241,100.723339,-28.750151,-28.464152 +100723632.209,100.723632,-28.510303,-28.466920 +100723925.178,100.723925,-28.680735,-28.468159 +100724218.146,100.724218,-29.028772,-28.466567 +100724511.115,100.724511,-28.514524,-28.464981 +100724804.083,100.724804,-28.494076,-28.472601 +100725097.052,100.725097,-27.771662,-28.477701 +100725390.021,100.725390,-28.446609,-28.477890 +100725682.989,100.725683,-28.892355,-28.475902 +100725975.958,100.725976,-28.455061,-28.477953 +100726268.926,100.726269,-28.129606,-28.478382 +100726561.895,100.726562,-28.277803,-28.472996 +100726854.863,100.726855,-28.303183,-28.471529 +100727147.832,100.727148,-28.563656,-28.466103 +100727440.800,100.727441,-28.780510,-28.461911 +100727733.769,100.727734,-29.221306,-28.462863 +100728026.737,100.728027,-28.810373,-28.467299 +100728319.706,100.728320,-28.208204,-28.469487 +100728612.674,100.728613,-28.084995,-28.466532 +100728905.643,100.728906,-28.486727,-28.461542 +100729198.611,100.729199,-28.717720,-28.460806 +100729491.580,100.729492,-28.837282,-28.459751 +100729784.548,100.729785,-28.658545,-28.463032 +100730077.517,100.730078,-28.436941,-28.467275 +100730370.485,100.730370,-28.276608,-28.469522 +100730663.454,100.730663,-28.191130,-28.473746 +100730956.422,100.730956,-28.528643,-28.479350 +100731249.391,100.731249,-28.712523,-28.478434 +100731542.359,100.731542,-28.292528,-28.480931 +100731835.328,100.731835,-28.600199,-28.482135 +100732128.296,100.732128,-28.970263,-28.481350 +100732421.265,100.732421,-28.708239,-28.481703 +100732714.233,100.732714,-28.393915,-28.481907 +100733007.202,100.733007,-28.917856,-28.481285 +100733300.170,100.733300,-28.238506,-28.480513 +100733593.139,100.733593,-28.015055,-28.483177 +100733886.107,100.733886,-28.351412,-28.488198 +100734179.076,100.734179,-28.194395,-28.493031 +100734472.044,100.734472,-27.945980,-28.494799 +100734765.013,100.734765,-27.821241,-28.493299 +100735057.981,100.735058,-28.095963,-28.494767 +100735350.950,100.735351,-28.471407,-28.497682 +100735643.918,100.735644,-28.059954,-28.500843 +100735936.887,100.735937,-28.438272,-28.505873 +100736229.855,100.736230,-28.630537,-28.503575 +100736522.824,100.736523,-28.603119,-28.501700 +100736815.792,100.736816,-28.785477,-28.504329 +100737108.761,100.737109,-28.845226,-28.504230 +100737401.729,100.737402,-28.501108,-28.501400 +100737694.698,100.737695,-28.582960,-28.498766 +100737987.666,100.737988,-28.446735,-28.496176 +100738280.635,100.738281,-28.206591,-28.496487 +100738573.603,100.738574,-28.410421,-28.496823 +100738866.572,100.738867,-28.482660,-28.497311 +100739159.540,100.739160,-28.080610,-28.502374 +100739452.509,100.739453,-28.767967,-28.506643 +100739745.477,100.739745,-28.587982,-28.504864 +100740038.446,100.740038,-28.548174,-28.508240 +100740331.414,100.740331,-28.267824,-28.510447 +100740624.383,100.740624,-28.191713,-28.510485 +100740917.351,100.740917,-28.463198,-28.510163 +100741210.320,100.741210,-28.406748,-28.511497 +100741503.288,100.741503,-28.554941,-28.516269 +100741796.257,100.741796,-28.439756,-28.516777 +100742089.225,100.742089,-28.685474,-28.514513 +100742382.194,100.742382,-28.289005,-28.515891 +100742675.162,100.742675,-28.501337,-28.521998 +100742968.131,100.742968,-28.460142,-28.520083 +100743261.099,100.743261,-28.558487,-28.517913 +100743554.068,100.743554,-28.692551,-28.516395 +100743847.036,100.743847,-28.793583,-28.520424 +100744140.005,100.744140,-28.581461,-28.522598 +100744432.973,100.744433,-28.180994,-28.518877 +100744725.942,100.744726,-28.704626,-28.518220 +100745018.910,100.745019,-28.827932,-28.518013 +100745311.879,100.745312,-28.328228,-28.524928 +100745604.847,100.745605,-28.120256,-28.526723 +100745897.816,100.745898,-28.632206,-28.527053 +100746190.784,100.746191,-28.367638,-28.528220 +100746483.753,100.746484,-27.875017,-28.531418 +100746776.721,100.746777,-28.344669,-28.533861 +100747069.690,100.747070,-28.224417,-28.534701 +100747362.658,100.747363,-28.206228,-28.532836 +100747655.627,100.747656,-28.520050,-28.527651 +100747948.595,100.747949,-28.705359,-28.518817 +100748241.564,100.748242,-28.576391,-28.511547 +100748534.532,100.748535,-28.138458,-28.509599 +100748827.501,100.748828,-28.150990,-28.514895 +100749120.469,100.749120,-28.292656,-28.519738 +100749413.438,100.749413,-28.452726,-28.521036 +100749706.406,100.749706,-28.372850,-28.521040 +100749999.375,100.749999,-28.123207,-28.520379 +100750292.344,100.750292,-28.050531,-28.520469 +100750585.312,100.750585,-28.824877,-28.524127 +100750878.281,100.750878,-29.400557,-28.527964 +100751171.249,100.751171,-28.479471,-28.530964 +100751464.218,100.751464,-28.258509,-28.534061 +100751757.186,100.751757,-28.281679,-28.540381 +100752050.155,100.752050,-28.401876,-28.544141 +100752343.123,100.752343,-28.787268,-28.541550 +100752636.092,100.752636,-28.716341,-28.540636 +100752929.060,100.752929,-28.703289,-28.543017 +100753222.029,100.753222,-28.530756,-28.544570 +100753514.997,100.753515,-29.080183,-28.545601 +100753807.966,100.753808,-29.134102,-28.550386 +100754100.934,100.754101,-28.809843,-28.551850 +100754393.903,100.754394,-28.492699,-28.549163 +100754686.871,100.754687,-28.334421,-28.551421 +100754979.840,100.754980,-28.560009,-28.557883 +100755272.808,100.755273,-28.355112,-28.559958 +100755565.777,100.755566,-28.593645,-28.562105 +100755858.745,100.755859,-29.276590,-28.570617 +100756151.714,100.756152,-28.721809,-28.570412 +100756444.682,100.756445,-28.800245,-28.568486 +100756737.651,100.756738,-29.152319,-28.568301 +100757030.619,100.757031,-28.331797,-28.569820 +100757323.588,100.757324,-28.117296,-28.570823 +100757616.556,100.757617,-28.459856,-28.574043 +100757909.525,100.757910,-28.614313,-28.576159 +100758202.493,100.758202,-28.720411,-28.577164 +100758495.462,100.758495,-28.725090,-28.576562 +100758788.430,100.758788,-28.743650,-28.576572 +100759081.399,100.759081,-29.689749,-28.577623 +100759374.367,100.759374,-28.962000,-28.581266 +100759667.336,100.759667,-28.363741,-28.578557 +100759960.304,100.759960,-28.735765,-28.573159 +100760253.273,100.760253,-28.920816,-28.567817 +100760546.241,100.760546,-28.690233,-28.571050 +100760839.210,100.760839,-28.389206,-28.574645 +100761132.178,100.761132,-28.677158,-28.576821 +100761425.147,100.761425,-28.874390,-28.580642 +100761718.115,100.761718,-28.507608,-28.583150 +100762011.084,100.762011,-28.499977,-28.582730 +100762304.052,100.762304,-28.739456,-28.582222 +100762597.021,100.762597,-29.090403,-28.584264 +100762889.989,100.762890,-28.542770,-28.585555 +100763182.958,100.763183,-28.543287,-28.585640 +100763475.926,100.763476,-28.536037,-28.583619 +100763768.895,100.763769,-29.099483,-28.579797 +100764061.863,100.764062,-29.001200,-28.572986 +100764354.832,100.764355,-28.490492,-28.571134 +100764647.800,100.764648,-28.425547,-28.568741 +100764940.769,100.764941,-28.465958,-28.564116 +100765233.737,100.765234,-28.652342,-28.560641 +100765526.706,100.765527,-28.695120,-28.562844 +100765819.674,100.765820,-28.941755,-28.567256 +100766112.643,100.766113,-28.615290,-28.571744 +100766405.611,100.766406,-28.548658,-28.575807 +100766698.580,100.766699,-28.605522,-28.580814 +100766991.548,100.766992,-28.413746,-28.579633 +100767284.517,100.767285,-28.313688,-28.578009 +100767577.485,100.767577,-28.086489,-28.578312 +100767870.454,100.767870,-27.991793,-28.576790 +100768163.422,100.768163,-27.857096,-28.576474 +100768456.391,100.768456,-27.966118,-28.578090 +100768749.359,100.768749,-28.798046,-28.584643 +100769042.328,100.769042,-29.201208,-28.588092 +100769335.296,100.769335,-28.907919,-28.585728 +100769628.265,100.769628,-28.837921,-28.584818 +100769921.233,100.769921,-28.565926,-28.585327 +100770214.202,100.770214,-28.449060,-28.587428 +100770507.170,100.770507,-28.778137,-28.589214 +100770800.139,100.770800,-28.707268,-28.582801 +100771093.107,100.771093,-28.960382,-28.571109 +100771386.076,100.771386,-29.178881,-28.569937 +100771679.044,100.771679,-29.199362,-28.569415 +100771972.013,100.771972,-29.156303,-28.569743 +100772264.981,100.772265,-28.595249,-28.571558 +100772557.950,100.772558,-28.580112,-28.570944 +100772850.918,100.772851,-28.721205,-28.567153 +100773143.887,100.773144,-29.156059,-28.564196 +100773436.855,100.773437,-28.372175,-28.564023 +100773729.824,100.773730,-28.637480,-28.562913 +100774022.792,100.774023,-28.547268,-28.557827 +100774315.761,100.774316,-27.867908,-28.552327 +100774608.729,100.774609,-28.223856,-28.552896 +100774901.698,100.774902,-28.641764,-28.554556 +100775194.667,100.775195,-28.359329,-28.551526 +100775487.635,100.775488,-28.769613,-28.551537 +100775780.604,100.775781,-29.245848,-28.549135 +100776073.572,100.776074,-28.411152,-28.543596 +100776366.541,100.776367,-28.370861,-28.543746 +100776659.509,100.776660,-28.577635,-28.542082 +100776952.478,100.776952,-29.009893,-28.539287 +100777245.446,100.777245,-28.994171,-28.541585 +100777538.415,100.777538,-28.958200,-28.546906 +100777831.383,100.777831,-28.883556,-28.544948 +100778124.352,100.778124,-28.582541,-28.535270 +100778417.320,100.778417,-28.131428,-28.527920 +100778710.289,100.778710,-28.411808,-28.523737 +100779003.257,100.779003,-28.625807,-28.520837 +100779296.226,100.779296,-28.550026,-28.514736 +100779589.194,100.779589,-28.396759,-28.507358 +100779882.163,100.779882,-27.903292,-28.496691 +100780175.131,100.780175,-27.875071,-28.483180 +100780468.100,100.780468,-28.702059,-28.474599 +100780761.068,100.780761,-28.668034,-28.463509 +100781054.037,100.781054,-28.763918,-28.457618 +100781347.005,100.781347,-28.941446,-28.454097 +100781639.974,100.781640,-28.910553,-28.449734 +100781932.942,100.781933,-28.384411,-28.445104 +100782225.911,100.782226,-28.614901,-28.440340 +100782518.879,100.782519,-28.558577,-28.431305 +100782811.848,100.782812,-28.678411,-28.422603 +100783104.816,100.783105,-28.471405,-28.420824 +100783397.785,100.783398,-28.291750,-28.421436 +100783690.753,100.783691,-28.185997,-28.418463 +100783983.722,100.783984,-27.909128,-28.406211 +100784276.690,100.784277,-28.334393,-28.389857 +100784569.659,100.784570,-27.891150,-28.379931 +100784862.627,100.784863,-28.095266,-28.371934 +100785155.596,100.785156,-28.349291,-28.363778 +100785448.564,100.785449,-28.624025,-28.350617 +100785741.533,100.785742,-28.703348,-28.340027 +100786034.501,100.786035,-29.305685,-28.327650 +100786327.470,100.786327,-28.933821,-28.314514 +100786620.438,100.786620,-28.501444,-28.299200 +100786913.407,100.786913,-28.194038,-28.286048 +100787206.375,100.787206,-28.024050,-28.275832 +100787499.344,100.787499,-28.244482,-28.265536 +100787792.312,100.787792,-28.319103,-28.262901 +100788085.281,100.788085,-28.660969,-28.260285 +100788378.249,100.788378,-28.803616,-28.257845 +100788671.218,100.788671,-29.035742,-28.255035 +100788964.186,100.788964,-28.601034,-28.246554 +100789257.155,100.789257,-28.000004,-28.233653 +100789550.123,100.789550,-28.333523,-28.221934 +100789843.092,100.789843,-28.439711,-28.209681 +100790136.060,100.790136,-28.389914,-28.197220 +100790429.029,100.790429,-28.272324,-28.184332 +100790721.997,100.790722,-27.983696,-28.168279 +100791014.966,100.791015,-27.799282,-28.150074 +100791307.934,100.791308,-28.324997,-28.133939 +100791600.903,100.791601,-28.192482,-28.118104 +100791893.871,100.791894,-28.323896,-28.102148 +100792186.840,100.792187,-28.647985,-28.090961 +100792479.808,100.792480,-28.699656,-28.084554 +100792772.777,100.792773,-28.209749,-28.072701 +100793065.745,100.793066,-28.304106,-28.057023 +100793358.714,100.793359,-28.507294,-28.046125 +100793651.682,100.793652,-28.912260,-28.038381 +100793944.651,100.793945,-28.403816,-28.026789 +100794237.619,100.794238,-28.076431,-28.015897 +100794530.588,100.794531,-28.570215,-28.006895 +100794823.556,100.794824,-28.556192,-27.995892 +100795116.525,100.795117,-28.163006,-27.983749 +100795409.493,100.795409,-28.356554,-27.975596 +100795702.462,100.795702,-28.273367,-27.964630 +100795995.430,100.795995,-28.460550,-27.950422 +100796288.399,100.796288,-28.743298,-27.940215 +100796581.367,100.796581,-28.564743,-27.930284 +100796874.336,100.796874,-28.732491,-27.917253 +100797167.304,100.797167,-28.642759,-27.901003 +100797460.273,100.797460,-28.834101,-27.888112 +100797753.241,100.797753,-28.204481,-27.878036 +100798046.210,100.798046,-27.439751,-27.864145 +100798339.178,100.798339,-27.778658,-27.852617 +100798632.147,100.798632,-28.163090,-27.838216 +100798925.115,100.798925,-28.344477,-27.814082 +100799218.084,100.799218,-28.713350,-27.787600 +100799511.052,100.799511,-27.964497,-27.767019 +100799804.021,100.799804,-27.138525,-27.747169 +100800096.990,100.800097,-27.118990,-27.731807 +100800389.958,100.800390,-27.782465,-27.716467 +100800682.927,100.800683,-27.328947,-27.697513 +100800975.895,100.800976,-27.660028,-27.678034 +100801268.864,100.801269,-28.197090,-27.655564 +100801561.832,100.801562,-28.261068,-27.630476 +100801854.801,100.801855,-27.907879,-27.606307 +100802147.769,100.802148,-27.884392,-27.583549 +100802440.738,100.802441,-27.581160,-27.556840 +100802733.706,100.802734,-27.885237,-27.523969 +100803026.675,100.803027,-28.299109,-27.488644 +100803319.643,100.803320,-28.630415,-27.459358 +100803612.612,100.803613,-28.136639,-27.434668 +100803905.580,100.803906,-27.477602,-27.403109 +100804198.549,100.804199,-26.979713,-27.363700 +100804491.517,100.804492,-27.282057,-27.326115 +100804784.486,100.804784,-27.438517,-27.299919 +100805077.454,100.805077,-27.451326,-27.270651 +100805370.423,100.805370,-27.055582,-27.235343 +100805663.391,100.805663,-27.353090,-27.196767 +100805956.360,100.805956,-27.330727,-27.156145 +100806249.328,100.806249,-27.021400,-27.108966 +100806542.297,100.806542,-26.756689,-27.060184 +100806835.265,100.806835,-27.004232,-27.020800 +100807128.234,100.807128,-27.167860,-26.982196 +100807421.202,100.807421,-27.081594,-26.938870 +100807714.171,100.807714,-27.753099,-26.887518 +100808007.139,100.808007,-27.667379,-26.842126 +100808300.108,100.808300,-27.562582,-26.793178 +100808593.076,100.808593,-27.620249,-26.744325 +100808886.045,100.808886,-27.647001,-26.690065 +100809179.013,100.809179,-27.417879,-26.636965 +100809471.982,100.809472,-27.347418,-26.588202 +100809764.950,100.809765,-27.236042,-26.536819 +100810057.919,100.810058,-27.020744,-26.481040 +100810350.887,100.810351,-26.891222,-26.433966 +100810643.856,100.810644,-26.787033,-26.388872 +100810936.824,100.810937,-26.528408,-26.342743 +100811229.793,100.811230,-26.879915,-26.302697 +100811522.761,100.811523,-27.039564,-26.261466 +100811815.730,100.811816,-27.032852,-26.211275 +100812108.698,100.812109,-27.541370,-26.155785 +100812401.667,100.812402,-27.706226,-26.098754 +100812694.635,100.812695,-27.058256,-26.047567 +100812987.604,100.812988,-26.747660,-25.994270 +100813280.572,100.813281,-27.562365,-25.939071 +100813573.541,100.813574,-27.356142,-25.879755 +100813866.509,100.813867,-27.113680,-25.808931 +100814159.478,100.814159,-27.123941,-25.742951 +100814452.446,100.814452,-26.813679,-25.677225 +100814745.415,100.814745,-26.871363,-25.609115 +100815038.383,100.815038,-27.042803,-25.544650 +100815331.352,100.815331,-27.284939,-25.489270 +100815624.320,100.815624,-27.259489,-25.431976 +100815917.289,100.815917,-27.196550,-25.369745 +100816210.257,100.816210,-27.080721,-25.300351 +100816503.226,100.816503,-27.079367,-25.227173 +100816796.194,100.816796,-26.881306,-25.151420 +100817089.163,100.817089,-26.807501,-25.073104 +100817382.131,100.817382,-27.164806,-24.999436 +100817675.100,100.817675,-27.474861,-24.907777 +100817968.068,100.817968,-26.971193,-24.819650 +100818261.037,100.818261,-27.030243,-24.741821 +100818554.005,100.818554,-26.413557,-24.664134 +100818846.974,100.818847,-25.690573,-24.586579 +100819139.942,100.819140,-25.593512,-24.506205 +100819432.911,100.819433,-26.056410,-24.420996 +100819725.879,100.819726,-26.027500,-24.339631 +100820018.848,100.820019,-26.133013,-24.254843 +100820311.816,100.820312,-26.111258,-24.174893 +100820604.785,100.820605,-26.266949,-24.091741 +100820897.753,100.820898,-26.188162,-23.999568 +100821190.722,100.821191,-25.947870,-23.896984 +100821483.690,100.821484,-25.787563,-23.807066 +100821776.659,100.821777,-25.842806,-23.717979 +100822069.627,100.822070,-25.686865,-23.622402 +100822362.596,100.822363,-25.451679,-23.514073 +100822655.564,100.822656,-24.920837,-23.413817 +100822948.533,100.822949,-24.769737,-23.310588 +100823241.501,100.823242,-25.122156,-23.199438 +100823534.470,100.823534,-25.400110,-23.098402 +100823827.438,100.823827,-24.785769,-23.016937 +100824120.407,100.824120,-24.101315,-22.939609 +100824413.375,100.824413,-24.374132,-22.846850 +100824706.344,100.824706,-25.004124,-22.747260 +100824999.312,100.824999,-24.847111,-22.644514 +100825292.281,100.825292,-24.486761,-22.531627 +100825585.250,100.825585,-24.337990,-22.414127 +100825878.218,100.825878,-24.198866,-22.320763 +100826171.187,100.826171,-23.921526,-22.233579 +100826464.155,100.826464,-23.675976,-22.153673 +100826757.124,100.826757,-24.125181,-22.080720 +100827050.092,100.827050,-24.039980,-22.005194 +100827343.061,100.827343,-23.637114,-21.918659 +100827636.029,100.827636,-23.191578,-21.832537 +100827928.998,100.827929,-23.545759,-21.752557 +100828221.966,100.828222,-23.396917,-21.667434 +100828514.935,100.828515,-23.410158,-21.575052 +100828807.903,100.828808,-23.103680,-21.479187 +100829100.872,100.829101,-23.017702,-21.383456 +100829393.840,100.829394,-23.062267,-21.287402 +100829686.809,100.829687,-22.971720,-21.197993 +100829979.777,100.829980,-22.705610,-21.114313 +100830272.746,100.830273,-23.182201,-21.039541 +100830565.714,100.830566,-23.243322,-20.965268 +100830858.683,100.830859,-23.053213,-20.878016 +100831151.651,100.831152,-23.366995,-20.795514 +100831444.620,100.831445,-23.436855,-20.713224 +100831737.588,100.831738,-22.771517,-20.622145 +100832030.557,100.832031,-22.451916,-20.523030 +100832323.525,100.832324,-22.400457,-20.431824 +100832616.494,100.832616,-22.726727,-20.339159 +100832909.462,100.832909,-22.431326,-20.251351 +100833202.431,100.833202,-22.302326,-20.166781 +100833495.399,100.833495,-22.069479,-20.072920 +100833788.368,100.833788,-21.486561,-19.977476 +100834081.336,100.834081,-21.580610,-19.887221 +100834374.305,100.834374,-21.472242,-19.792327 +100834667.273,100.834667,-21.397211,-19.702491 +100834960.242,100.834960,-21.533365,-19.602775 +100835253.210,100.835253,-21.917204,-19.505978 +100835546.179,100.835546,-21.804419,-19.413651 +100835839.147,100.835839,-21.457657,-19.302329 +100836132.116,100.836132,-21.060219,-19.189832 +100836425.084,100.836425,-20.858984,-19.082847 +100836718.053,100.836718,-20.641806,-18.969223 +100837011.021,100.837011,-20.480759,-18.851402 +100837303.990,100.837304,-20.627554,-18.746320 +100837596.958,100.837597,-19.767555,-18.640767 +100837889.927,100.837890,-19.753674,-18.544730 +100838182.895,100.838183,-20.013437,-18.455825 +100838475.864,100.838476,-20.014742,-18.359514 +100838768.832,100.838769,-20.017321,-18.255953 +100839061.801,100.839062,-19.844473,-18.146351 +100839354.769,100.839355,-19.600893,-18.049110 +100839647.738,100.839648,-19.602119,-17.943514 +100839940.706,100.839941,-19.253120,-17.830671 +100840233.675,100.840234,-19.394070,-17.727046 +100840526.643,100.840527,-19.283621,-17.618175 +100840819.612,100.840820,-18.760048,-17.508024 +100841112.580,100.841113,-18.311594,-17.396925 +100841405.549,100.841406,-18.794790,-17.278340 +100841698.517,100.841699,-18.758406,-17.161757 +100841991.486,100.841991,-18.365673,-17.055724 +100842284.454,100.842284,-17.784672,-16.946869 +100842577.423,100.842577,-17.961411,-16.828672 +100842870.391,100.842870,-17.787827,-16.721564 +100843163.360,100.843163,-17.435074,-16.621406 +100843456.328,100.843456,-17.745222,-16.523294 +100843749.297,100.843749,-18.463118,-16.419439 +100844042.265,100.844042,-18.515625,-16.317212 +100844335.234,100.844335,-17.686487,-16.224425 +100844628.202,100.844628,-17.359224,-16.139712 +100844921.171,100.844921,-17.160986,-16.049421 +100845214.139,100.845214,-16.691339,-15.960121 +100845507.108,100.845507,-16.393171,-15.868687 +100845800.076,100.845800,-17.231876,-15.774401 +100846093.045,100.846093,-17.416170,-15.672653 +100846386.013,100.846386,-17.645605,-15.543649 +100846678.982,100.846679,-17.893009,-15.432105 +100846971.950,100.846972,-17.726778,-15.327968 +100847264.919,100.847265,-17.145472,-15.214985 +100847557.887,100.847558,-17.077887,-15.116528 +100847850.856,100.847851,-17.357512,-15.007359 +100848143.824,100.848144,-17.028074,-14.883810 +100848436.793,100.848437,-16.612207,-14.771419 +100848729.761,100.848730,-16.381601,-14.663635 +100849022.730,100.849023,-16.300512,-14.548684 +100849315.698,100.849316,-16.181208,-14.446113 +100849608.667,100.849609,-16.377413,-14.338335 +100849901.635,100.849902,-16.552622,-14.216606 +100850194.604,100.850195,-16.905237,-14.092316 +100850487.573,100.850488,-16.851486,-13.970134 +100850780.541,100.850781,-16.133736,-13.839254 +100851073.510,100.851074,-16.254520,-13.696300 +100851366.478,100.851366,-16.221283,-13.550995 +100851659.447,100.851659,-15.748822,-13.400078 +100851952.415,100.851952,-15.318728,-13.243816 +100852245.384,100.852245,-15.600663,-13.102895 +100852538.352,100.852538,-15.459951,-12.945943 +100852831.321,100.852831,-15.552069,-12.797951 +100853124.289,100.853124,-15.600477,-12.667482 +100853417.258,100.853417,-15.148983,-12.521116 +100853710.226,100.853710,-14.979449,-12.382006 +100854003.195,100.854003,-15.105689,-12.224484 +100854296.163,100.854296,-14.814928,-12.055769 +100854589.132,100.854589,-14.932331,-11.900563 +100854882.100,100.854882,-14.421164,-11.734876 +100855175.069,100.855175,-14.459839,-11.573282 +100855468.037,100.855468,-14.577770,-11.420561 +100855761.006,100.855761,-13.706699,-11.260260 +100856053.974,100.856054,-13.553314,-11.112265 +100856346.943,100.856347,-13.651829,-10.975293 +100856639.911,100.856640,-13.296185,-10.818462 +100856932.880,100.856933,-13.026064,-10.672728 +100857225.848,100.857226,-13.391658,-10.526223 +100857518.817,100.857519,-13.287777,-10.383150 +100857811.785,100.857812,-13.597299,-10.245967 +100858104.754,100.858105,-13.806773,-10.111386 +100858397.722,100.858398,-13.389494,-9.979708 +100858690.691,100.858691,-12.965408,-9.842976 +100858983.659,100.858984,-12.595159,-9.715197 +100859276.628,100.859277,-12.984797,-9.584934 +100859569.596,100.859570,-12.571213,-9.453322 +100859862.565,100.859863,-12.189608,-9.325420 +100860155.533,100.860156,-12.444878,-9.194990 +100860448.502,100.860449,-12.136519,-9.072516 +100860741.470,100.860741,-11.988474,-8.954445 +100861034.439,100.861034,-11.843189,-8.845683 +100861327.407,100.861327,-11.450836,-8.747709 +100861620.376,100.861620,-11.403011,-8.648002 +100861913.344,100.861913,-11.694463,-8.554615 +100862206.313,100.862206,-11.475471,-8.470002 +100862499.281,100.862499,-11.013031,-8.385535 +100862792.250,100.862792,-11.294766,-8.280817 +100863085.218,100.863085,-11.467263,-8.187484 +100863378.187,100.863378,-11.474028,-8.100006 +100863671.155,100.863671,-11.151182,-8.010738 +100863964.124,100.863964,-11.093565,-7.910422 +100864257.092,100.864257,-11.368618,-7.802686 +100864550.061,100.864550,-11.673458,-7.692352 +100864843.029,100.864843,-11.356964,-7.599034 +100865135.998,100.865136,-11.310094,-7.502966 +100865428.966,100.865429,-11.109598,-7.398735 +100865721.935,100.865722,-10.886842,-7.305328 +100866014.903,100.866015,-10.470753,-7.217664 +100866307.872,100.866308,-9.358008,-7.129310 +100866600.840,100.866601,-9.840527,-7.040957 +100866893.809,100.866894,-10.041554,-6.950783 +100867186.777,100.867187,-9.592990,-6.859854 +100867479.746,100.867480,-10.049452,-6.778024 +100867772.714,100.867773,-9.501660,-6.689772 +100868065.683,100.868066,-8.886558,-6.601017 +100868358.651,100.868359,-9.163774,-6.527684 +100868651.620,100.868652,-9.233256,-6.447631 +100868944.588,100.868945,-8.845502,-6.363208 +100869237.557,100.869238,-9.212979,-6.275887 +100869530.525,100.869531,-8.907300,-6.200919 +100869823.494,100.869823,-8.283899,-6.123452 +100870116.462,100.870116,-8.068653,-6.044440 +100870409.431,100.870409,-8.036749,-5.962597 +100870702.399,100.870702,-7.625663,-5.885162 +100870995.368,100.870995,-7.114186,-5.817410 +100871288.336,100.871288,-6.911894,-5.749320 +100871581.305,100.871581,-6.607512,-5.679280 +100871874.273,100.871874,-6.294939,-5.600396 +100872167.242,100.872167,-6.580218,-5.518445 +100872460.210,100.872460,-5.977042,-5.453618 +100872753.179,100.872753,-6.084854,-5.394830 +100873046.147,100.873046,-6.476479,-5.336297 +100873339.116,100.873339,-5.850861,-5.285071 +100873632.084,100.873632,-5.921713,-5.221930 +100873925.053,100.873925,-5.236009,-5.154642 +100874218.021,100.874218,-4.787115,-5.083517 +100874510.990,100.874511,-4.980943,-5.011594 +100874803.958,100.874804,-4.544629,-4.944693 +100875096.927,100.875097,-4.493506,-4.884488 +100875389.896,100.875390,-4.587288,-4.824472 +100875682.864,100.875683,-4.224932,-4.763798 +100875975.833,100.875976,-4.407697,-4.708590 +100876268.801,100.876269,-4.588917,-4.655341 +100876561.770,100.876562,-3.865306,-4.595100 +100876854.738,100.876855,-4.024725,-4.533212 +100877147.707,100.877148,-3.855881,-4.476276 +100877440.675,100.877441,-3.818062,-4.424443 +100877733.644,100.877734,-3.839662,-4.367581 +100878026.612,100.878027,-3.787928,-4.314723 +100878319.581,100.878320,-3.756794,-4.268029 +100878612.549,100.878613,-3.465892,-4.220369 +100878905.518,100.878906,-3.624150,-4.167065 +100879198.486,100.879198,-3.412438,-4.113524 +100879491.455,100.879491,-3.235202,-4.062849 +100879784.423,100.879784,-3.229776,-4.012457 +100880077.392,100.880077,-3.012432,-3.963829 +100880370.360,100.880370,-3.159230,-3.915477 +100880663.329,100.880663,-3.194337,-3.873643 +100880956.297,100.880956,-3.417027,-3.833615 +100881249.266,100.881249,-3.741785,-3.799134 +100881542.234,100.881542,-3.588148,-3.762839 +100881835.203,100.881835,-3.769099,-3.723592 +100882128.171,100.882128,-4.083420,-3.686748 +100882421.140,100.882421,-3.986446,-3.651786 +100882714.108,100.882714,-3.002575,-3.620035 +100883007.077,100.883007,-3.383904,-3.584533 +100883300.045,100.883300,-3.553893,-3.546812 +100883593.014,100.883593,-3.396439,-3.511367 +100883885.982,100.883886,-2.836864,-3.477129 +100884178.951,100.884179,-2.435021,-3.443788 +100884471.919,100.884472,-2.206203,-3.410605 +100884764.888,100.884765,-2.801807,-3.376330 +100885057.856,100.885058,-2.581761,-3.345537 +100885350.825,100.885351,-2.126898,-3.313469 +100885643.793,100.885644,-2.478736,-3.282536 +100885936.762,100.885937,-2.688533,-3.254546 +100886229.730,100.886230,-2.576894,-3.224845 +100886522.699,100.886523,-2.498663,-3.197677 +100886815.667,100.886816,-2.333163,-3.165875 +100887108.636,100.887109,-2.205303,-3.134497 +100887401.604,100.887402,-2.547115,-3.107223 +100887694.573,100.887695,-2.145414,-3.080929 +100887987.541,100.887988,-2.044310,-3.053660 +100888280.510,100.888281,-2.756444,-3.030733 +100888573.478,100.888573,-2.300461,-3.007692 +100888866.447,100.888866,-1.990467,-2.982947 +100889159.415,100.889159,-1.763361,-2.956983 +100889452.384,100.889452,-2.318064,-2.931571 +100889745.352,100.889745,-2.115669,-2.902710 +100890038.321,100.890038,-1.963914,-2.877582 +100890331.289,100.890331,-1.749724,-2.855666 +100890624.258,100.890624,-1.903781,-2.832748 +100890917.226,100.890917,-2.366363,-2.811320 +100891210.195,100.891210,-2.285372,-2.791090 +100891503.163,100.891503,-2.099934,-2.771892 +100891796.132,100.891796,-1.513630,-2.756705 +100892089.100,100.892089,-1.260387,-2.743783 +100892382.069,100.892382,-2.176997,-2.733248 +100892675.037,100.892675,-2.515687,-2.724053 +100892968.006,100.892968,-2.482494,-2.714612 +100893260.974,100.893261,-2.981991,-2.704372 +100893553.943,100.893554,-2.043213,-2.695132 +100893846.911,100.893847,-1.709968,-2.686068 +100894139.880,100.894140,-1.419458,-2.680503 +100894432.848,100.894433,-1.292087,-2.675072 +100894725.817,100.894726,-1.531293,-2.669690 +100895018.785,100.895019,-1.879936,-2.667225 +100895311.754,100.895312,-1.838227,-2.665919 +100895604.722,100.895605,-1.742527,-2.663802 +100895897.691,100.895898,-2.021666,-2.663876 +100896190.659,100.896191,-2.106532,-2.664373 +100896483.628,100.896484,-1.559934,-2.661665 +100896776.596,100.896777,-1.370587,-2.663679 +100897069.565,100.897070,-1.634284,-2.665695 +100897362.533,100.897363,-1.988499,-2.669628 +100897655.502,100.897656,-1.556656,-2.673986 +100897948.470,100.897948,-1.822034,-2.676575 +100898241.439,100.898241,-2.291617,-2.679242 +100898534.407,100.898534,-2.135148,-2.681604 +100898827.376,100.898827,-1.606828,-2.690017 +100899120.344,100.899120,-1.511789,-2.699428 +100899413.313,100.899413,-1.714000,-2.708332 +100899706.281,100.899706,-1.657666,-2.719255 +100899999.250,100.899999,-1.721338,-2.730894 +100900292.219,100.900292,-1.721886,-2.745210 +100900585.187,100.900585,-2.222647,-2.758797 +100900878.156,100.900878,-2.341949,-2.770389 +100901171.124,100.901171,-2.863822,-2.780377 +100901464.093,100.901464,-2.588628,-2.789196 +100901757.061,100.901757,-2.252674,-2.797908 +100902050.030,100.902050,-2.495707,-2.806900 +100902342.998,100.902343,-2.635884,-2.816180 +100902635.967,100.902636,-2.900497,-2.825654 +100902928.935,100.902929,-2.496182,-2.840840 +100903221.904,100.903222,-2.255746,-2.852742 +100903514.872,100.903515,-2.461620,-2.863113 +100903807.841,100.903808,-2.519699,-2.876320 +100904100.809,100.904101,-2.581834,-2.894929 +100904393.778,100.904394,-2.608663,-2.916418 +100904686.746,100.904687,-2.495052,-2.937904 +100904979.715,100.904980,-2.830059,-2.954241 +100905272.683,100.905273,-2.644722,-2.974237 +100905565.652,100.905566,-2.723994,-2.997895 +100905858.620,100.905859,-3.031789,-3.020067 +100906151.589,100.906152,-2.723923,-3.042281 +100906444.557,100.906445,-2.797776,-3.064998 +100906737.526,100.906738,-2.321595,-3.087879 +100907030.494,100.907030,-2.378677,-3.112559 +100907323.463,100.907323,-2.763782,-3.138456 +100907616.431,100.907616,-2.961331,-3.160038 +100907909.400,100.907909,-2.703017,-3.183894 +100908202.368,100.908202,-3.113970,-3.209581 +100908495.337,100.908495,-3.152435,-3.229827 +100908788.305,100.908788,-2.916186,-3.255743 +100909081.274,100.909081,-2.645618,-3.286168 +100909374.242,100.909374,-2.783379,-3.318918 +100909667.211,100.909667,-2.264289,-3.346358 +100909960.179,100.909960,-2.549935,-3.375857 +100910253.148,100.910253,-2.896251,-3.407778 +100910546.116,100.910546,-2.734943,-3.442780 +100910839.085,100.910839,-2.792535,-3.475956 +100911132.053,100.911132,-2.764161,-3.503759 +100911425.022,100.911425,-2.816560,-3.532705 +100911717.990,100.911718,-3.261584,-3.563565 +100912010.959,100.912011,-3.457414,-3.600517 +100912303.927,100.912304,-4.015535,-3.641803 +100912596.896,100.912597,-3.901224,-3.674174 +100912889.864,100.912890,-3.919515,-3.703878 +100913182.833,100.913183,-3.999099,-3.734910 +100913475.801,100.913476,-3.803906,-3.763517 +100913768.770,100.913769,-3.875972,-3.800620 +100914061.738,100.914062,-4.043365,-3.841040 +100914354.707,100.914355,-3.720335,-3.885844 +100914647.675,100.914648,-3.878722,-3.932830 +100914940.644,100.914941,-4.053384,-3.977224 +100915233.612,100.915234,-4.229127,-4.017643 +100915526.581,100.915527,-4.157715,-4.059435 +100915819.549,100.915820,-4.239500,-4.103185 +100916112.518,100.916113,-4.510621,-4.144206 +100916405.486,100.916405,-4.046280,-4.185693 +100916698.455,100.916698,-4.245512,-4.235047 +100916991.423,100.916991,-4.419737,-4.287557 +100917284.392,100.917284,-4.629324,-4.336706 +100917577.360,100.917577,-4.674506,-4.381416 +100917870.329,100.917870,-4.330253,-4.432582 +100918163.297,100.918163,-4.287275,-4.481117 +100918456.266,100.918456,-4.192526,-4.524435 +100918749.234,100.918749,-5.125669,-4.570568 +100919042.203,100.919042,-5.615030,-4.624081 +100919335.171,100.919335,-5.156004,-4.681074 +100919628.140,100.919628,-5.371395,-4.736956 +100919921.108,100.919921,-5.538850,-4.795079 +100920214.077,100.920214,-5.861178,-4.853177 +100920507.045,100.920507,-5.922577,-4.912412 +100920800.014,100.920800,-5.439887,-4.965841 +100921092.982,100.921093,-5.390857,-5.018256 +100921385.951,100.921386,-5.591225,-5.064830 +100921678.919,100.921679,-5.327322,-5.115335 +100921971.888,100.921972,-5.669427,-5.170748 +100922264.856,100.922265,-6.246837,-5.223747 +100922557.825,100.922558,-6.139340,-5.275879 +100922850.793,100.922851,-5.996329,-5.324724 +100923143.762,100.923144,-5.786312,-5.380410 +100923436.730,100.923437,-5.658107,-5.440913 +100923729.699,100.923730,-6.156708,-5.498543 +100924022.667,100.924023,-6.609264,-5.556603 +100924315.636,100.924316,-6.504952,-5.614809 +100924608.604,100.924609,-5.913312,-5.673912 +100924901.573,100.924902,-5.799109,-5.736138 +100925194.542,100.925195,-6.324791,-5.794516 +100925487.510,100.925488,-6.242425,-5.856918 +100925780.479,100.925780,-6.669688,-5.918816 +100926073.447,100.926073,-7.218143,-5.977107 +100926366.416,100.926366,-7.039457,-6.040659 +100926659.384,100.926659,-6.838971,-6.104027 +100926952.353,100.926952,-6.923278,-6.176440 +100927245.321,100.927245,-6.902834,-6.248978 +100927538.290,100.927538,-6.447445,-6.316001 +100927831.258,100.927831,-6.061510,-6.381170 +100928124.227,100.928124,-6.263228,-6.451231 +100928417.195,100.928417,-6.462932,-6.515548 +100928710.164,100.928710,-6.960975,-6.580633 +100929003.132,100.929003,-7.437338,-6.650908 +100929296.101,100.929296,-7.367308,-6.727174 +100929589.069,100.929589,-7.338827,-6.802779 +100929882.038,100.929882,-7.323317,-6.890318 +100930175.006,100.930175,-7.567739,-6.973448 +100930467.975,100.930468,-7.782600,-7.051274 +100930760.943,100.930761,-7.614145,-7.134010 +100931053.912,100.931054,-7.276143,-7.217359 +100931346.880,100.931347,-7.336338,-7.302796 +100931639.849,100.931640,-7.291660,-7.389236 +100931932.817,100.931933,-7.104702,-7.468661 +100932225.786,100.932226,-7.390817,-7.545799 +100932518.754,100.932519,-7.801105,-7.614612 +100932811.723,100.932812,-7.970055,-7.686730 +100933104.691,100.933105,-8.271127,-7.759461 +100933397.660,100.933398,-9.113220,-7.831511 +100933690.628,100.933691,-8.704279,-7.908820 +100933983.597,100.933984,-8.370030,-7.986693 +100934276.565,100.934277,-8.521714,-8.062559 +100934569.534,100.934570,-8.534274,-8.145886 +100934862.502,100.934863,-8.553012,-8.228117 +100935155.471,100.935155,-8.515779,-8.308523 +100935448.439,100.935448,-8.706640,-8.386789 +100935741.408,100.935741,-8.852249,-8.468153 +100936034.376,100.936034,-8.957705,-8.549797 +100936327.345,100.936327,-9.428171,-8.628009 +100936620.313,100.936620,-9.676641,-8.716901 +100936913.282,100.936913,-9.648768,-8.802943 +100937206.250,100.937206,-9.509657,-8.887473 +100937499.219,100.937499,-9.366698,-8.969831 +100937792.187,100.937792,-9.443108,-9.053153 +100938085.156,100.938085,-9.638124,-9.145703 +100938378.124,100.938378,-9.770234,-9.241252 +100938671.093,100.938671,-9.961715,-9.341137 +100938964.061,100.938964,-9.785655,-9.422821 +100939257.030,100.939257,-10.450891,-9.497050 +100939549.998,100.939550,-11.089189,-9.581494 +100939842.967,100.939843,-11.499845,-9.663087 +100940135.935,100.940136,-11.595467,-9.742907 +100940428.904,100.940429,-11.829773,-9.818457 +100940721.872,100.940722,-12.297823,-9.893878 +100941014.841,100.941015,-12.265483,-9.980267 +100941307.809,100.941308,-12.443491,-10.069688 +100941600.778,100.941601,-12.402980,-10.156832 +100941893.746,100.941894,-12.187246,-10.251559 +100942186.715,100.942187,-12.389957,-10.340725 +100942479.683,100.942480,-12.673052,-10.420224 +100942772.652,100.942773,-12.358602,-10.503289 +100943065.620,100.943066,-12.953600,-10.591043 +100943358.589,100.943359,-13.393867,-10.685464 +100943651.557,100.943652,-12.893050,-10.785336 +100943944.526,100.943945,-13.291150,-10.876014 +100944237.494,100.944237,-13.520582,-10.958983 +100944530.463,100.944530,-13.962409,-11.045446 +100944823.431,100.944823,-14.527531,-11.147170 +100945116.400,100.945116,-14.825048,-11.254687 +100945409.368,100.945409,-15.346743,-11.351943 +100945702.337,100.945702,-15.118472,-11.453228 +100945995.305,100.945995,-15.218727,-11.547112 +100946288.274,100.946288,-15.012694,-11.631148 +100946581.242,100.946581,-15.051756,-11.720677 +100946874.211,100.946874,-15.177699,-11.816807 +100947167.179,100.947167,-15.091788,-11.912709 +100947460.148,100.947460,-15.008780,-12.011516 +100947753.116,100.947753,-15.360355,-12.124639 +100948046.085,100.948046,-14.917052,-12.251622 +100948339.053,100.948339,-14.914814,-12.376514 +100948632.022,100.948632,-15.329268,-12.499293 +100948924.990,100.948925,-15.573399,-12.611284 +100949217.959,100.949218,-15.507581,-12.713795 +100949510.927,100.949511,-16.111731,-12.820581 +100949803.896,100.949804,-16.401743,-12.930894 +100950096.865,100.950097,-16.126221,-13.044246 +100950389.833,100.950390,-16.047167,-13.154225 +100950682.802,100.950683,-16.391033,-13.262018 +100950975.770,100.950976,-16.596638,-13.377314 +100951268.739,100.951269,-16.449089,-13.505604 +100951561.707,100.951562,-16.903505,-13.635739 +100951854.676,100.951855,-17.214527,-13.771518 +100952147.644,100.952148,-17.193678,-13.918451 +100952440.613,100.952441,-17.514011,-14.060516 +100952733.581,100.952734,-17.824945,-14.193411 +100953026.550,100.953027,-17.451708,-14.325404 +100953319.518,100.953320,-16.862331,-14.452066 +100953612.487,100.953612,-17.388617,-14.558857 +100953905.455,100.953905,-18.072519,-14.679940 +100954198.424,100.954198,-17.697357,-14.814693 +100954491.392,100.954491,-17.482222,-14.948722 +100954784.361,100.954784,-18.074625,-15.086592 +100955077.329,100.955077,-18.145514,-15.228651 +100955370.298,100.955370,-17.839447,-15.377093 +100955663.266,100.955663,-18.167906,-15.524036 +100955956.235,100.955956,-18.635649,-15.670757 +100956249.203,100.956249,-19.030190,-15.818451 +100956542.172,100.956542,-18.793364,-15.955459 +100956835.140,100.956835,-18.210905,-16.088752 +100957128.109,100.957128,-18.680552,-16.227012 +100957421.077,100.957421,-19.240263,-16.374487 +100957714.046,100.957714,-19.663305,-16.532454 +100958007.014,100.958007,-20.121519,-16.693498 +100958299.983,100.958300,-20.135349,-16.853813 +100958592.951,100.958593,-20.050114,-17.015701 +100958885.920,100.958886,-19.947012,-17.176081 +100959178.888,100.959179,-20.313009,-17.349236 +100959471.857,100.959472,-20.520697,-17.503160 +100959764.825,100.959765,-20.210529,-17.639959 +100960057.794,100.960058,-20.129566,-17.767787 +100960350.762,100.960351,-20.616917,-17.896990 +100960643.731,100.960644,-20.099886,-18.023029 +100960936.699,100.960937,-20.330435,-18.138833 +100961229.668,100.961230,-20.685181,-18.258692 +100961522.636,100.961523,-20.885656,-18.377104 +100961815.605,100.961816,-20.879148,-18.500107 +100962108.573,100.962109,-20.801645,-18.633360 +100962401.542,100.962402,-21.216061,-18.764085 +100962694.510,100.962695,-21.162615,-18.889661 +100962987.479,100.962987,-21.341305,-19.029888 +100963280.447,100.963280,-21.734442,-19.155751 +100963573.416,100.963573,-22.378626,-19.271798 +100963866.384,100.963866,-22.196239,-19.406174 +100964159.353,100.964159,-21.924341,-19.532211 +100964452.321,100.964452,-21.504860,-19.655666 +100964745.290,100.964745,-21.583969,-19.769580 +100965038.258,100.965038,-22.333546,-19.870899 +100965331.227,100.965331,-22.346777,-19.967594 +100965624.195,100.965624,-22.061310,-20.055171 +100965917.164,100.965917,-22.492886,-20.150047 +100966210.132,100.966210,-22.301056,-20.243809 +100966503.101,100.966503,-22.429628,-20.345107 +100966796.069,100.966796,-22.790297,-20.447463 +100967089.038,100.967089,-22.105104,-20.548780 +100967382.006,100.967382,-22.479748,-20.654727 +100967674.975,100.967675,-23.066170,-20.765731 +100967967.943,100.967968,-22.505049,-20.871410 +100968260.912,100.968261,-22.867811,-20.991617 +100968553.880,100.968554,-23.145769,-21.115521 +100968846.849,100.968847,-22.864944,-21.230426 +100969139.817,100.969140,-22.804661,-21.340806 +100969432.786,100.969433,-22.829611,-21.455609 +100969725.754,100.969726,-22.943680,-21.557916 +100970018.723,100.970019,-22.610277,-21.655875 +100970311.691,100.970312,-22.907557,-21.763480 +100970604.660,100.970605,-23.795843,-21.875011 +100970897.628,100.970898,-24.134012,-21.979722 +100971190.597,100.971191,-23.974833,-22.081784 +100971483.565,100.971484,-23.727266,-22.189655 +100971776.534,100.971777,-23.979597,-22.288035 +100972069.502,100.972070,-24.518137,-22.382307 +100972362.471,100.972362,-24.555655,-22.479533 +100972655.439,100.972655,-24.175732,-22.570218 +100972948.408,100.972948,-24.649446,-22.655797 +100973241.376,100.973241,-24.629045,-22.753039 +100973534.345,100.973534,-24.852346,-22.869492 +100973827.313,100.973827,-25.363029,-22.973319 +100974120.282,100.974120,-25.137743,-23.062579 +100974413.250,100.974413,-25.016939,-23.163109 +100974706.219,100.974706,-24.998556,-23.271538 +100974999.188,100.974999,-25.415537,-23.366268 +100975292.156,100.975292,-25.799049,-23.462119 +100975585.125,100.975585,-25.942955,-23.569321 +100975878.093,100.975878,-25.700308,-23.669413 +100976171.062,100.976171,-25.230457,-23.759793 +100976464.030,100.976464,-25.779448,-23.844050 +100976756.999,100.976757,-25.779848,-23.934935 +100977049.967,100.977050,-25.567154,-24.042860 +100977342.936,100.977343,-25.363026,-24.139812 +100977635.904,100.977636,-25.326990,-24.226212 +100977928.873,100.977929,-25.430500,-24.305772 +100978221.841,100.978222,-26.233057,-24.376766 +100978514.810,100.978515,-27.010597,-24.448823 +100978807.778,100.978808,-26.697990,-24.524216 +100979100.747,100.979101,-26.087002,-24.602986 +100979393.715,100.979394,-26.367815,-24.674259 +100979686.684,100.979687,-26.326925,-24.743063 +100979979.652,100.979980,-26.241796,-24.819224 +100980272.621,100.980273,-26.747225,-24.898520 +100980565.589,100.980566,-26.950741,-24.969354 +100980858.558,100.980859,-26.811903,-25.054802 +100981151.526,100.981152,-26.760393,-25.135655 +100981444.495,100.981444,-27.107471,-25.210487 +100981737.463,100.981737,-27.182348,-25.283402 +100982030.432,100.982030,-27.077553,-25.355445 +100982323.400,100.982323,-26.924423,-25.429325 +100982616.369,100.982616,-26.701761,-25.498169 +100982909.337,100.982909,-27.244892,-25.570180 +100983202.306,100.983202,-27.624495,-25.638235 +100983495.274,100.983495,-27.213692,-25.700402 +100983788.243,100.983788,-26.972145,-25.751368 +100984081.211,100.984081,-27.123346,-25.806036 +100984374.180,100.984374,-27.709908,-25.865817 +100984667.148,100.984667,-27.537657,-25.933727 +100984960.117,100.984960,-27.112909,-26.000369 +100985253.085,100.985253,-27.335770,-26.054802 +100985546.054,100.985546,-27.847307,-26.112690 +100985839.022,100.985839,-28.122339,-26.176666 +100986131.991,100.986132,-27.441050,-26.231145 +100986424.959,100.986425,-27.773012,-26.290769 +100986717.928,100.986718,-27.509007,-26.348356 +100987010.896,100.987011,-27.312296,-26.400744 +100987303.865,100.987304,-27.296694,-26.467654 +100987596.833,100.987597,-27.388906,-26.528645 +100987889.802,100.987890,-28.256304,-26.581827 +100988182.770,100.988183,-27.872766,-26.646964 +100988475.739,100.988476,-28.009350,-26.702537 +100988768.707,100.988769,-27.838049,-26.753793 +100989061.676,100.989062,-27.416309,-26.811332 +100989354.644,100.989355,-27.240808,-26.870813 +100989647.613,100.989648,-27.777456,-26.931136 +100989940.581,100.989941,-28.084854,-26.990439 +100990233.550,100.990234,-28.237011,-27.055565 +100990526.518,100.990527,-27.683939,-27.115726 +100990819.487,100.990819,-27.538826,-27.163172 +100991112.455,100.991112,-27.636412,-27.207142 +100991405.424,100.991405,-27.349916,-27.252652 +100991698.392,100.991698,-27.288664,-27.303828 +100991991.361,100.991991,-27.903765,-27.351853 +100992284.329,100.992284,-28.073423,-27.390595 +100992577.298,100.992577,-27.618692,-27.428289 +100992870.266,100.992870,-27.671799,-27.473389 +100993163.235,100.993163,-28.169041,-27.511450 +100993456.203,100.993456,-28.647051,-27.548002 +100993749.172,100.993749,-28.154659,-27.583740 +100994042.140,100.994042,-28.146662,-27.614440 +100994335.109,100.994335,-28.221325,-27.648067 +100994628.077,100.994628,-28.099188,-27.681951 +100994921.046,100.994921,-27.793306,-27.717381 +100995214.014,100.995214,-28.094973,-27.744650 +100995506.983,100.995507,-28.555996,-27.770375 +100995799.951,100.995800,-28.163933,-27.794228 +100996092.920,100.996093,-28.042356,-27.819615 +100996385.888,100.996386,-28.456854,-27.849942 +100996678.857,100.996679,-28.254812,-27.874610 +100996971.825,100.996972,-28.213343,-27.900326 +100997264.794,100.997265,-27.884457,-27.930036 +100997557.762,100.997558,-28.213793,-27.964855 +100997850.731,100.997851,-28.513643,-27.995211 +100998143.699,100.998144,-28.291512,-28.022818 +100998436.668,100.998437,-28.343624,-28.044509 +100998729.636,100.998730,-28.475897,-28.058588 +100999022.605,100.999023,-28.433582,-28.076254 +100999315.573,100.999316,-28.019751,-28.103323 +100999608.542,100.999609,-28.152250,-28.128155 +100999901.510,100.999902,-28.134493,-28.151281 +101000194.479,101.000194,-28.140329,-28.174489 +101000487.448,101.000487,-28.235422,-28.191966 +101000780.416,101.000780,-28.954184,-28.206310 +101001073.385,101.001073,-28.611563,-28.223722 +101001366.353,101.001366,-28.641947,-28.241228 +101001659.322,101.001659,-28.916142,-28.251917 +101001952.290,101.001952,-28.267357,-28.262633 +101002245.259,101.002245,-28.010042,-28.280811 +101002538.227,101.002538,-28.583416,-28.300357 +101002831.196,101.002831,-28.863491,-28.319432 +101003124.164,101.003124,-28.429146,-28.335294 +101003417.133,101.003417,-28.564783,-28.345409 +101003710.101,101.003710,-28.293842,-28.359033 +101004003.070,101.004003,-28.319368,-28.377893 +101004296.038,101.004296,-28.236013,-28.394571 +101004589.007,101.004589,-28.055450,-28.405522 +101004881.975,101.004882,-27.896732,-28.418124 +101005174.944,101.005175,-28.076576,-28.436907 +101005467.912,101.005468,-28.774603,-28.453291 +101005760.881,101.005761,-28.801474,-28.463305 +101006053.849,101.006054,-28.199415,-28.470538 +101006346.818,101.006347,-28.422365,-28.482201 +101006639.786,101.006640,-28.309660,-28.489621 +101006932.755,101.006933,-28.381340,-28.500756 +101007225.723,101.007226,-28.660736,-28.516148 +101007518.692,101.007519,-28.727161,-28.529684 +101007811.660,101.007812,-29.108963,-28.538052 +101008104.629,101.008105,-29.444918,-28.539821 +101008397.597,101.008398,-28.548334,-28.546601 +101008690.566,101.008691,-28.501890,-28.551464 +101008983.534,101.008984,-28.656092,-28.557863 +101009276.503,101.009277,-28.667702,-28.572744 +101009569.471,101.009569,-28.768044,-28.590774 +101009862.440,101.009862,-28.824697,-28.603449 +101010155.408,101.010155,-28.523512,-28.611376 +101010448.377,101.010448,-28.517023,-28.617427 +101010741.345,101.010741,-28.925545,-28.630991 +101011034.314,101.011034,-29.178925,-28.645864 +101011327.282,101.011327,-28.902935,-28.658245 +101011620.251,101.011620,-29.143774,-28.671702 +101011913.219,101.011913,-29.219742,-28.683879 +101012206.188,101.012206,-28.878574,-28.694749 +101012499.156,101.012499,-28.716221,-28.705830 +101012792.125,101.012792,-28.981123,-28.720669 +101013085.093,101.013085,-28.896515,-28.731382 +101013378.062,101.013378,-28.514618,-28.737524 +101013671.030,101.013671,-28.870501,-28.738116 +101013963.999,101.013964,-29.117245,-28.743691 +101014256.967,101.014257,-29.089224,-28.748912 +101014549.936,101.014550,-28.793995,-28.755145 +101014842.904,101.014843,-28.978729,-28.763477 +101015135.873,101.015136,-28.467150,-28.771885 +101015428.841,101.015429,-28.959127,-28.781442 +101015721.810,101.015722,-28.889891,-28.785857 +101016014.778,101.016015,-28.650537,-28.791530 +101016307.747,101.016308,-28.465504,-28.798290 +101016600.715,101.016601,-28.633738,-28.802395 +101016893.684,101.016894,-28.784315,-28.805672 +101017186.652,101.017187,-28.994730,-28.812582 +101017479.621,101.017480,-29.405535,-28.824514 +101017772.589,101.017773,-28.513266,-28.831739 +101018065.558,101.018066,-28.278393,-28.834600 +101018358.526,101.018359,-28.845417,-28.841147 +101018651.495,101.018651,-28.877968,-28.846218 +101018944.463,101.018944,-28.964266,-28.853061 +101019237.432,101.019237,-29.444613,-28.857981 +101019530.400,101.019530,-29.589760,-28.865558 +101019823.369,101.019823,-29.171772,-28.874411 +101020116.337,101.020116,-29.002207,-28.885708 +101020409.306,101.020409,-28.937628,-28.895443 +101020702.274,101.020702,-28.751671,-28.898715 +101020995.243,101.020995,-29.013121,-28.899773 +101021288.211,101.021288,-28.932013,-28.905923 +101021581.180,101.021581,-28.411524,-28.909342 +101021874.148,101.021874,-28.513149,-28.913346 +101022167.117,101.022167,-29.561426,-28.922850 +101022460.085,101.022460,-29.522175,-28.932678 +101022753.054,101.022753,-29.034029,-28.939015 +101023046.022,101.023046,-29.391603,-28.936899 +101023338.991,101.023339,-28.994801,-28.938321 +101023631.959,101.023632,-28.957150,-28.943742 +101023924.928,101.023925,-29.423897,-28.947599 +101024217.896,101.024218,-29.294075,-28.954600 +101024510.865,101.024511,-29.226105,-28.968016 +101024803.833,101.024804,-29.245007,-28.979811 +101025096.802,101.025097,-29.613680,-28.991150 +101025389.771,101.025390,-29.556684,-29.000049 +101025682.739,101.025683,-29.240591,-29.003783 +101025975.708,101.025976,-29.150211,-29.005717 +101026268.676,101.026269,-28.927767,-29.012766 +101026561.645,101.026562,-28.734636,-29.021010 +101026854.613,101.026855,-28.935165,-29.027171 +101027147.582,101.027148,-29.309610,-29.034806 +101027440.550,101.027441,-28.984079,-29.040063 +101027733.519,101.027734,-28.373243,-29.045190 +101028026.487,101.028026,-28.489611,-29.045019 +101028319.456,101.028319,-28.752008,-29.042146 +101028612.424,101.028612,-28.641573,-29.047356 +101028905.393,101.028905,-28.653328,-29.052972 +101029198.361,101.029198,-29.356638,-29.056715 +101029491.330,101.029491,-29.589056,-29.062225 +101029784.298,101.029784,-29.523741,-29.063489 +101030077.267,101.030077,-29.173378,-29.065478 +101030370.235,101.030370,-29.072422,-29.068373 +101030663.204,101.030663,-29.518801,-29.071338 +101030956.172,101.030956,-29.505829,-29.073175 +101031249.141,101.031249,-29.242458,-29.074219 +101031542.109,101.031542,-28.984150,-29.075137 +101031835.078,101.031835,-28.709053,-29.077486 +101032128.046,101.032128,-29.367750,-29.079227 +101032421.015,101.032421,-29.638437,-29.083608 +101032713.983,101.032714,-29.582939,-29.091686 +101033006.952,101.033007,-29.009562,-29.094021 +101033299.920,101.033300,-28.978767,-29.098840 +101033592.889,101.033593,-28.727268,-29.108262 +101033885.857,101.033886,-28.879221,-29.110936 +101034178.826,101.034179,-28.819204,-29.111380 +101034471.794,101.034472,-29.051462,-29.109134 +101034764.763,101.034765,-29.210457,-29.111287 +101035057.731,101.035058,-28.827669,-29.110339 +101035350.700,101.035351,-29.389223,-29.112719 +101035643.668,101.035644,-29.171473,-29.112722 +101035936.637,101.035937,-28.895334,-29.110486 +101036229.605,101.036230,-28.900570,-29.112575 +101036522.574,101.036523,-29.010366,-29.121196 +101036815.542,101.036816,-28.669554,-29.124987 +101037108.511,101.037109,-29.128696,-29.126486 +101037401.479,101.037401,-29.455566,-29.129053 +101037694.448,101.037694,-29.171345,-29.131036 +101037987.416,101.037987,-28.893909,-29.140730 +101038280.385,101.038280,-29.164541,-29.151554 +101038573.353,101.038573,-29.011650,-29.154774 +101038866.322,101.038866,-29.434504,-29.161284 +101039159.290,101.039159,-29.092802,-29.167573 +101039452.259,101.039452,-28.972054,-29.169153 +101039745.227,101.039745,-29.325542,-29.166165 +101040038.196,101.040038,-29.684397,-29.169311 +101040331.164,101.040331,-29.438395,-29.174596 +101040624.133,101.040624,-28.638510,-29.176064 +101040917.101,101.040917,-29.103571,-29.179157 +101041210.070,101.041210,-29.480883,-29.180436 +101041503.038,101.041503,-29.106600,-29.187027 +101041796.007,101.041796,-29.503145,-29.197534 +101042088.975,101.042089,-29.563860,-29.205638 +101042381.944,101.042382,-29.265062,-29.203691 +101042674.912,101.042675,-29.468319,-29.202903 +101042967.881,101.042968,-28.587486,-29.207364 +101043260.849,101.043261,-28.605909,-29.207723 +101043553.818,101.043554,-29.305641,-29.209653 +101043846.786,101.043847,-28.774214,-29.212574 +101044139.755,101.044140,-29.239683,-29.216840 +101044432.723,101.044433,-30.161394,-29.220427 +101044725.692,101.044726,-29.611599,-29.222672 +101045018.660,101.045019,-29.313639,-29.225655 +101045311.629,101.045312,-29.195894,-29.222188 +101045604.597,101.045605,-29.289398,-29.219538 +101045897.566,101.045898,-29.061930,-29.218382 +101046190.534,101.046191,-29.085331,-29.217047 +101046483.503,101.046484,-29.539492,-29.218901 +101046776.471,101.046776,-29.092066,-29.224040 +101047069.440,101.047069,-29.390793,-29.228154 +101047362.408,101.047362,-29.377041,-29.229943 +101047655.377,101.047655,-29.435036,-29.233768 +101047948.345,101.047948,-29.085169,-29.245141 +101048241.314,101.048241,-29.033329,-29.255936 +101048534.282,101.048534,-29.236811,-29.259860 +101048827.251,101.048827,-29.239313,-29.265467 +101049120.219,101.049120,-29.151031,-29.271345 +101049413.188,101.049413,-29.418764,-29.273051 +101049706.156,101.049706,-28.933027,-29.271191 +101049999.125,101.049999,-29.090479,-29.267492 +101050292.094,101.050292,-28.888464,-29.267096 +101050585.062,101.050585,-28.890181,-29.270193 +101050878.031,101.050878,-29.175978,-29.269604 +101051170.999,101.051171,-29.328020,-29.267948 +101051463.968,101.051464,-29.025497,-29.270924 +101051756.936,101.051757,-29.483583,-29.274757 +101052049.905,101.052050,-29.473455,-29.278361 +101052342.873,101.052343,-29.492788,-29.274525 +101052635.842,101.052636,-29.873543,-29.271177 +101052928.810,101.052929,-29.304750,-29.272588 +101053221.779,101.053222,-29.577648,-29.278102 +101053514.747,101.053515,-29.819557,-29.283508 +101053807.716,101.053808,-29.231882,-29.291096 +101054100.684,101.054101,-29.178595,-29.294676 +101054393.653,101.054394,-28.793306,-29.300535 +101054686.621,101.054687,-29.077229,-29.303762 +101054979.590,101.054980,-28.854603,-29.306123 +101055272.558,101.055273,-28.757805,-29.309035 +101055565.527,101.055566,-28.959412,-29.304466 +101055858.495,101.055858,-28.608145,-29.307143 +101056151.464,101.056151,-28.915941,-29.313569 +101056444.432,101.056444,-29.623564,-29.318332 +101056737.401,101.056737,-29.124559,-29.321248 +101057030.369,101.057030,-28.978388,-29.328331 +101057323.338,101.057323,-29.349886,-29.328852 +101057616.306,101.057616,-29.705172,-29.325572 +101057909.275,101.057909,-29.850105,-29.323172 +101058202.243,101.058202,-29.702719,-29.329401 +101058495.212,101.058495,-29.277176,-29.333152 +101058788.180,101.058788,-29.806608,-29.335890 +101059081.149,101.059081,-29.876541,-29.338138 +101059374.117,101.059374,-29.681608,-29.343036 +101059667.086,101.059667,-29.160559,-29.346493 +101059960.054,101.059960,-29.626184,-29.345081 +101060253.023,101.060253,-29.761099,-29.341258 +101060545.991,101.060546,-29.132387,-29.343764 +101060838.960,101.060839,-29.153824,-29.353932 +101061131.928,101.061132,-29.185156,-29.357139 +101061424.897,101.061425,-29.880579,-29.353749 +101061717.865,101.061718,-29.819569,-29.352599 +101062010.834,101.062011,-29.582045,-29.348469 +101062303.802,101.062304,-29.281034,-29.343487 +101062596.771,101.062597,-29.407526,-29.340790 +101062889.739,101.062890,-29.665327,-29.336660 +101063182.708,101.063183,-29.443270,-29.339168 +101063475.676,101.063476,-29.254078,-29.341416 +101063768.645,101.063769,-29.351984,-29.341268 +101064061.613,101.064062,-30.085028,-29.346444 +101064354.582,101.064355,-29.825253,-29.349578 +101064647.550,101.064648,-29.545519,-29.347058 +101064940.519,101.064941,-29.676352,-29.347023 +101065233.487,101.065233,-29.122982,-29.347393 +101065526.456,101.065526,-29.181044,-29.346127 +101065819.424,101.065819,-29.084307,-29.343438 +101066112.393,101.066112,-28.973763,-29.342619 +101066405.361,101.066405,-29.172106,-29.344548 +101066698.330,101.066698,-29.414543,-29.344062 +101066991.298,101.066991,-29.497189,-29.348064 +101067284.267,101.067284,-29.566786,-29.350285 +101067577.235,101.067577,-29.509584,-29.354377 +101067870.204,101.067870,-29.884432,-29.357634 +101068163.172,101.068163,-29.951464,-29.361666 +101068456.141,101.068456,-29.258902,-29.364667 +101068749.109,101.068749,-29.364302,-29.364368 +101069042.078,101.069042,-29.415466,-29.364654 +101069335.046,101.069335,-29.601698,-29.365225 +101069628.015,101.069628,-29.323246,-29.363077 +101069920.983,101.069921,-29.017214,-29.364937 +101070213.952,101.070214,-29.120523,-29.367292 +101070506.920,101.070507,-29.498138,-29.366960 +101070799.889,101.070800,-29.434195,-29.366956 +101071092.857,101.071093,-29.272598,-29.365590 +101071385.826,101.071386,-29.667904,-29.363273 +101071678.794,101.071679,-29.505568,-29.364956 +101071971.763,101.071972,-29.165419,-29.364331 +101072264.731,101.072265,-28.861732,-29.363968 +101072557.700,101.072558,-29.166147,-29.361871 +101072850.668,101.072851,-29.795786,-29.354682 +101073143.637,101.073144,-29.785536,-29.352092 +101073436.605,101.073437,-29.731134,-29.345862 +101073729.574,101.073730,-29.753227,-29.338523 +101074022.542,101.074023,-29.349663,-29.339577 +101074315.511,101.074316,-29.605284,-29.340361 +101074608.479,101.074608,-29.490185,-29.349020 +101074901.448,101.074901,-29.539003,-29.353090 +101075194.417,101.075194,-29.200438,-29.357258 +101075487.385,101.075487,-28.794046,-29.360643 +101075780.354,101.075780,-29.542456,-29.363273 +101076073.322,101.076073,-29.779669,-29.367597 +101076366.291,101.076366,-29.538145,-29.365404 +101076659.259,101.076659,-29.399328,-29.355862 +101076952.228,101.076952,-29.596167,-29.357182 +101077245.196,101.077245,-29.197474,-29.360952 +101077538.165,101.077538,-29.015770,-29.357361 +101077831.133,101.077831,-28.864979,-29.350930 +101078124.102,101.078124,-29.744560,-29.344075 +101078417.070,101.078417,-29.689451,-29.342742 +101078710.039,101.078710,-29.374651,-29.346223 +101079003.007,101.079003,-29.761454,-29.340582 +101079295.976,101.079296,-29.778021,-29.331039 +101079588.944,101.079589,-29.430002,-29.324036 +101079881.913,101.079882,-29.137096,-29.322025 +101080174.881,101.080175,-29.151939,-29.316336 +101080467.850,101.080468,-29.804348,-29.306175 +101080760.818,101.080761,-30.019297,-29.303895 +101081053.787,101.081054,-29.539320,-29.302077 +101081346.755,101.081347,-29.027388,-29.301697 +101081639.724,101.081640,-28.960213,-29.297878 +101081932.692,101.081933,-28.953470,-29.292602 +101082225.661,101.082226,-28.899656,-29.285460 +101082518.629,101.082519,-28.916403,-29.282295 +101082811.598,101.082812,-28.921337,-29.277023 +101083104.566,101.083105,-28.886625,-29.273096 +101083397.535,101.083398,-28.874096,-29.268990 +101083690.503,101.083691,-29.285566,-29.266818 +101083983.472,101.083983,-29.445498,-29.259688 +101084276.440,101.084276,-29.679985,-29.249107 +101084569.409,101.084569,-29.763834,-29.241663 +101084862.377,101.084862,-29.606531,-29.234621 +101085155.346,101.085155,-29.364286,-29.226752 +101085448.314,101.085448,-29.031458,-29.224359 +101085741.283,101.085741,-28.940098,-29.222826 +101086034.251,101.086034,-28.957991,-29.221492 +101086327.220,101.086327,-29.341793,-29.218731 +101086620.188,101.086620,-29.470499,-29.208487 +101086913.157,101.086913,-29.642553,-29.196129 +101087206.125,101.087206,-29.709539,-29.181962 +101087499.094,101.087499,-29.980835,-29.168397 +101087792.062,101.087792,-29.914999,-29.156661 +101088085.031,101.088085,-29.637424,-29.146710 +101088377.999,101.088378,-29.432165,-29.128982 +101088670.968,101.088671,-29.197233,-29.113022 +101088963.936,101.088964,-29.277582,-29.101678 +101089256.905,101.089257,-29.226038,-29.090550 +101089549.873,101.089550,-29.130615,-29.075428 +101089842.842,101.089843,-29.170122,-29.061847 +101090135.810,101.090136,-29.404366,-29.051170 +101090428.779,101.090429,-28.847885,-29.033806 +101090721.747,101.090722,-28.889717,-29.013585 +101091014.716,101.091015,-29.000418,-28.994256 +101091307.684,101.091308,-29.024191,-28.979778 +101091600.653,101.091601,-29.244091,-28.961130 +101091893.621,101.091894,-29.396517,-28.938929 +101092186.590,101.092187,-29.422798,-28.913942 +101092479.558,101.092480,-29.206394,-28.888569 +101092772.527,101.092773,-28.885899,-28.868009 +101093065.495,101.093065,-28.967272,-28.836829 +101093358.464,101.093358,-28.760017,-28.797854 +101093651.432,101.093651,-28.821333,-28.765623 +101093944.401,101.093944,-29.375166,-28.731830 +101094237.369,101.094237,-29.283342,-28.693105 +101094530.338,101.094530,-29.988520,-28.654397 +101094823.306,101.094823,-29.635050,-28.608662 +101095116.275,101.095116,-29.395851,-28.553584 +101095409.243,101.095409,-29.181646,-28.506289 +101095702.212,101.095702,-29.300848,-28.461217 +101095995.180,101.095995,-29.136656,-28.414278 +101096288.149,101.096288,-28.653166,-28.364605 +101096581.117,101.096581,-28.418520,-28.309197 +101096874.086,101.096874,-29.299469,-28.248372 +101097167.054,101.097167,-29.479362,-28.188927 +101097460.023,101.097460,-28.884869,-28.133715 +101097752.991,101.097753,-28.841681,-28.076213 +101098045.960,101.098046,-28.906174,-28.017451 +101098338.928,101.098339,-29.508823,-27.949195 +101098631.897,101.098632,-29.774031,-27.875209 +101098924.865,101.098925,-29.022181,-27.804519 +101099217.834,101.099218,-28.602350,-27.729026 +101099510.802,101.099511,-28.748512,-27.652429 +101099803.771,101.099804,-28.903021,-27.570235 +101100096.740,101.100097,-28.861189,-27.477046 +101100389.708,101.100390,-28.440100,-27.390326 +101100682.677,101.100683,-28.842096,-27.301483 +101100975.645,101.100976,-28.919611,-27.205332 +101101268.614,101.101269,-29.134775,-27.122037 +101101561.582,101.101562,-29.320234,-27.044655 +101101854.551,101.101855,-29.072218,-26.951917 +101102147.519,101.102148,-28.644167,-26.848481 +101102440.488,101.102440,-28.867973,-26.741580 +101102733.456,101.102733,-28.720993,-26.638000 +101103026.425,101.103026,-29.113371,-26.526044 +101103319.393,101.103319,-28.894167,-26.417942 +101103612.362,101.103612,-28.967173,-26.309448 +101103905.330,101.103905,-28.454430,-26.169121 +101104198.299,101.104198,-28.611012,-26.033284 +101104491.267,101.104491,-28.792833,-25.904290 +101104784.236,101.104784,-28.616709,-25.776536 +101105077.204,101.105077,-28.621075,-25.653463 +101105370.173,101.105370,-28.814100,-25.509456 +101105663.141,101.105663,-28.977861,-25.367208 +101105956.110,101.105956,-28.910772,-25.231398 +101106249.078,101.106249,-28.630241,-25.075611 +101106542.047,101.106542,-27.966156,-24.894397 +101106835.015,101.106835,-27.924347,-24.721136 +101107127.984,101.107128,-27.795219,-24.569543 +101107420.952,101.107421,-27.898691,-24.409379 +101107713.921,101.107714,-28.045170,-24.252375 +101108006.889,101.108007,-28.512077,-24.092112 +101108299.858,101.108300,-27.712751,-23.932090 +101108592.826,101.108593,-27.441311,-23.778049 +101108885.795,101.108886,-27.966555,-23.630678 +101109178.763,101.109179,-28.023508,-23.471751 +101109471.732,101.109472,-27.733463,-23.282425 +101109764.700,101.109765,-27.696741,-23.081490 +101110057.669,101.110058,-27.766365,-22.887920 +101110350.637,101.110351,-27.195730,-22.677359 +101110643.606,101.110644,-27.164343,-22.473590 +101110936.574,101.110937,-27.198618,-22.265891 +101111229.543,101.111230,-27.547277,-22.033287 +101111522.511,101.111523,-27.387089,-21.809777 +101111815.480,101.111815,-26.962749,-21.596659 +101112108.448,101.112108,-26.534193,-21.345589 +101112401.417,101.112401,-26.325600,-21.092779 +101112694.385,101.112694,-26.879526,-20.867854 +101112987.354,101.112987,-26.327869,-20.636577 +101113280.322,101.113280,-25.743139,-20.417166 +101113573.291,101.113573,-26.180109,-20.204304 +101113866.259,101.113866,-26.054758,-19.955647 +101114159.228,101.114159,-25.521078,-19.668077 +101114452.196,101.114452,-25.603037,-19.436838 +101114745.165,101.114745,-25.084057,-19.264099 +101115038.133,101.115038,-24.533556,-19.068831 +101115331.102,101.115331,-24.821318,-18.848566 +101115624.070,101.115624,-24.767799,-18.629672 +101115917.039,101.115917,-24.904083,-18.412904 +101116210.007,101.116210,-24.785389,-18.232153 +101116502.976,101.116503,-24.346661,-18.101738 +101116795.944,101.116796,-23.979071,-17.970319 +101117088.913,101.117089,-24.063192,-17.797596 +101117381.881,101.117382,-24.131102,-17.627990 +101117674.850,101.117675,-23.914045,-17.464585 +101117967.818,101.117968,-23.762287,-17.329321 +101118260.787,101.118261,-23.481152,-17.210870 +101118553.755,101.118554,-23.143723,-17.080600 +101118846.724,101.118847,-23.167831,-16.934757 +101119139.692,101.119140,-22.976469,-16.820387 +101119432.661,101.119433,-22.869547,-16.724637 +101119725.629,101.119726,-22.491505,-16.624973 +101120018.598,101.120019,-21.925261,-16.523338 +101120311.566,101.120312,-22.107506,-16.428661 +101120604.535,101.120605,-22.069136,-16.331808 +101120897.503,101.120898,-21.736307,-16.241330 +101121190.472,101.121190,-22.103634,-16.148854 +101121483.440,101.121483,-22.197538,-16.048930 +101121776.409,101.121776,-21.481297,-15.955265 +101122069.377,101.122069,-21.004862,-15.867090 +101122362.346,101.122362,-20.787201,-15.784582 +101122655.314,101.122655,-20.816689,-15.704001 +101122948.283,101.122948,-20.437973,-15.618481 +101123241.251,101.123241,-20.469116,-15.532937 +101123534.220,101.123534,-20.360970,-15.458122 +101123827.188,101.123827,-19.317211,-15.389322 +101124120.157,101.124120,-19.335924,-15.324808 +101124413.125,101.124413,-19.441671,-15.262390 +101124706.094,101.124706,-19.371090,-15.196609 +101124999.062,101.124999,-19.389730,-15.133066 +101125292.031,101.125292,-18.625341,-15.073775 +101125585.000,101.125585,-18.514551,-15.007342 +101125877.968,101.125878,-18.562847,-14.946524 +101126170.937,101.126171,-17.885349,-14.884562 +101126463.905,101.126464,-17.143406,-14.823540 +101126756.874,101.126757,-17.167719,-14.767412 +101127049.842,101.127050,-17.570704,-14.713236 +101127342.811,101.127343,-17.203236,-14.657644 +101127635.779,101.127636,-17.149763,-14.601545 +101127928.748,101.127929,-16.911198,-14.546620 +101128221.716,101.128222,-16.751287,-14.493604 +101128514.685,101.128515,-16.748453,-14.440595 +101128807.653,101.128808,-16.774679,-14.386927 +101129100.622,101.129101,-16.323864,-14.337533 +101129393.590,101.129394,-15.430162,-14.292109 +101129686.559,101.129687,-14.990913,-14.253721 +101129979.527,101.129980,-14.958732,-14.215797 +101130272.496,101.130272,-14.419037,-14.170737 +101130565.464,101.130565,-14.337817,-14.127395 +101130858.433,101.130858,-14.060367,-14.086057 +101131151.401,101.131151,-13.373016,-14.042322 +101131444.370,101.131444,-13.320333,-14.001015 +101131737.338,101.131737,-13.314888,-13.963543 +101132030.307,101.132030,-12.395695,-13.922250 +101132323.275,101.132323,-12.119701,-13.884439 +101132616.244,101.132616,-12.378970,-13.847605 +101132909.212,101.132909,-12.030504,-13.804888 +101133202.181,101.133202,-12.035521,-13.764492 +101133495.149,101.133495,-11.948511,-13.724058 +101133788.118,101.133788,-11.060655,-13.682800 +101134081.086,101.134081,-10.181660,-13.646563 +101134374.055,101.134374,-10.859109,-13.611830 +101134667.023,101.134667,-11.918641,-13.572505 +101134959.992,101.134960,-11.207601,-13.533127 +101135252.960,101.135253,-10.483255,-13.496566 +101135545.929,101.135546,-10.290541,-13.463321 +101135838.897,101.135839,-10.118766,-13.430083 +101136131.866,101.136132,-10.699351,-13.392288 +101136424.834,101.136425,-11.930801,-13.354110 +101136717.803,101.136718,-11.765033,-13.313227 +101137010.771,101.137011,-10.463644,-13.273743 +101137303.740,101.137304,-10.375034,-13.237114 +101137596.708,101.137597,-10.362553,-13.202571 +101137889.677,101.137890,-11.023214,-13.163717 +101138182.645,101.138183,-11.466208,-13.124676 +101138475.614,101.138476,-10.946872,-13.089456 +101138768.582,101.138769,-10.329608,-13.051940 +101139061.551,101.139062,-11.232135,-13.013005 +101139354.519,101.139355,-11.878323,-12.972006 +101139647.488,101.139647,-11.615098,-12.936653 +101139940.456,101.139940,-11.435741,-12.904205 +101140233.425,101.140233,-11.640974,-12.869938 +101140526.393,101.140526,-11.442052,-12.833872 +101140819.362,101.140819,-11.648065,-12.796425 +101141112.330,101.141112,-11.466558,-12.759852 +101141405.299,101.141405,-11.044770,-12.721514 +101141698.267,101.141698,-11.228952,-12.678125 +101141991.236,101.141991,-11.393524,-12.635241 +101142284.204,101.142284,-11.585140,-12.596051 +101142577.173,101.142577,-11.610082,-12.559211 +101142870.141,101.142870,-11.272300,-12.523123 +101143163.110,101.143163,-11.193676,-12.486417 +101143456.078,101.143456,-11.682889,-12.443318 +101143749.047,101.143749,-11.970807,-12.401551 +101144042.015,101.144042,-12.167788,-12.364788 +101144334.984,101.144335,-12.249603,-12.325052 +101144627.952,101.144628,-11.967894,-12.283479 +101144920.921,101.144921,-12.047953,-12.241919 +101145213.889,101.145214,-12.282055,-12.199911 +101145506.858,101.145507,-11.741293,-12.161807 +101145799.826,101.145800,-12.058040,-12.123428 +101146092.795,101.146093,-11.917215,-12.087346 +101146385.763,101.146386,-11.916222,-12.053087 +101146678.732,101.146679,-12.197680,-12.021498 +101146971.700,101.146972,-12.292474,-11.987804 +101147264.669,101.147265,-12.126532,-11.952775 +101147557.637,101.147558,-12.036615,-11.916332 +101147850.606,101.147851,-12.075689,-11.875822 +101148143.574,101.148144,-12.184261,-11.835820 +101148436.543,101.148437,-12.112965,-11.798077 +101148729.511,101.148730,-12.001267,-11.765065 +101149022.480,101.149022,-12.316236,-11.728582 +101149315.448,101.149315,-12.625471,-11.691223 +101149608.417,101.149608,-13.282803,-11.655687 +101149901.385,101.149901,-13.295711,-11.624109 +101150194.354,101.150194,-12.533825,-11.593254 +101150487.323,101.150487,-12.635910,-11.557476 +101150780.291,101.150780,-12.792165,-11.521620 +101151073.260,101.151073,-12.516130,-11.488247 +101151366.228,101.151366,-12.726527,-11.460302 +101151659.197,101.151659,-13.091242,-11.434994 +101151952.165,101.151952,-12.631570,-11.406911 +101152245.134,101.152245,-12.943455,-11.385759 +101152538.102,101.152538,-13.007066,-11.369287 +101152831.071,101.152831,-12.374157,-11.351588 +101153124.039,101.153124,-12.546189,-11.338792 +101153417.008,101.153417,-12.477749,-11.325573 +101153709.976,101.153710,-12.374271,-11.310578 +101154002.945,101.154003,-12.870009,-11.298461 +101154295.913,101.154296,-12.983322,-11.297134 +101154588.882,101.154589,-12.441162,-11.298292 +101154881.850,101.154882,-12.371790,-11.290512 +101155174.819,101.155175,-12.607319,-11.280001 +101155467.787,101.155468,-12.979326,-11.272887 +101155760.756,101.155761,-12.945761,-11.269573 +101156053.724,101.156054,-12.396523,-11.268493 +101156346.693,101.156347,-12.312676,-11.262114 +101156639.661,101.156640,-11.970020,-11.243669 +101156932.630,101.156933,-12.051945,-11.223227 +101157225.598,101.157226,-12.326485,-11.214169 +101157518.567,101.157519,-12.535144,-11.209346 +101157811.535,101.157812,-12.009798,-11.206006 +101158104.504,101.158105,-11.944128,-11.198202 +101158397.472,101.158397,-12.305126,-11.186006 +101158690.441,101.158690,-11.992559,-11.173071 +101158983.409,101.158983,-11.809774,-11.165101 +101159276.378,101.159276,-11.550829,-11.154038 +101159569.346,101.159569,-12.100752,-11.137055 +101159862.315,101.159862,-12.374386,-11.121878 +101160155.283,101.160155,-12.074415,-11.108725 +101160448.252,101.160448,-11.860639,-11.097120 +101160741.220,101.160741,-11.675562,-11.087310 +101161034.189,101.161034,-11.703207,-11.075250 +101161327.157,101.161327,-11.517994,-11.060328 +101161620.126,101.161620,-10.994287,-11.045044 +101161913.094,101.161913,-10.942651,-11.028468 +101162206.063,101.162206,-11.213142,-11.011278 +101162499.031,101.162499,-11.394504,-10.995520 +101162792.000,101.162792,-11.444868,-10.981223 +101163084.968,101.163085,-11.302740,-10.964917 +101163377.937,101.163378,-10.652418,-10.946677 +101163670.905,101.163671,-10.724812,-10.925229 +101163963.874,101.163964,-11.040487,-10.900505 +101164256.842,101.164257,-10.720942,-10.880708 +101164549.811,101.164550,-10.529594,-10.864007 +101164842.779,101.164843,-10.485477,-10.847382 +101165135.748,101.165136,-10.410852,-10.830696 +101165428.716,101.165429,-10.631182,-10.809731 +101165721.685,101.165722,-10.555229,-10.790269 +101166014.653,101.166015,-10.756317,-10.768409 +101166307.622,101.166308,-10.790001,-10.747048 +101166600.590,101.166601,-10.880880,-10.724492 +101166893.559,101.166894,-10.645469,-10.701413 +101167186.527,101.167187,-10.570224,-10.675771 +101167479.496,101.167479,-10.330261,-10.653706 +101167772.464,101.167772,-9.919998,-10.638147 +101168065.433,101.168065,-9.885476,-10.626012 +101168358.401,101.168358,-10.022364,-10.614092 +101168651.370,101.168651,-10.446081,-10.602210 +101168944.338,101.168944,-10.088280,-10.590024 +101169237.307,101.169237,-9.879642,-10.574646 +101169530.275,101.169530,-9.792442,-10.558157 +101169823.244,101.169823,-10.001121,-10.534767 +101170116.212,101.170116,-10.038060,-10.511286 +101170409.181,101.170409,-9.405249,-10.496295 +101170702.149,101.170702,-9.348614,-10.482897 +101170995.118,101.170995,-9.442205,-10.466774 +101171288.086,101.171288,-9.634733,-10.453104 +101171581.055,101.171581,-9.839795,-10.436962 +101171874.023,101.171874,-9.567082,-10.423618 +101172166.992,101.172167,-9.747727,-10.412611 +101172459.960,101.172460,-10.034084,-10.395462 +101172752.929,101.172753,-10.059500,-10.377359 +101173045.897,101.173046,-10.349429,-10.365985 +101173338.866,101.173339,-10.302748,-10.352018 +101173631.834,101.173632,-10.053879,-10.336216 +101173924.803,101.173925,-9.720899,-10.320866 +101174217.771,101.174218,-10.043257,-10.307687 +101174510.740,101.174511,-11.004922,-10.297211 +101174803.708,101.174804,-10.835192,-10.286091 +101175096.677,101.175097,-9.983194,-10.274612 +101175389.646,101.175390,-9.738850,-10.261756 +101175682.614,101.175683,-9.942685,-10.246294 +101175975.583,101.175976,-10.006717,-10.229289 +101176268.551,101.176269,-9.992895,-10.220044 +101176561.520,101.176562,-9.677270,-10.211511 +101176854.488,101.176854,-9.387557,-10.204972 +101177147.457,101.177147,-9.528909,-10.196624 +101177440.425,101.177440,-9.861714,-10.185255 +101177733.394,101.177733,-10.001261,-10.175184 +101178026.362,101.178026,-10.102192,-10.165597 +101178319.331,101.178319,-9.974711,-10.155184 +101178612.299,101.178612,-9.526842,-10.144048 +101178905.268,101.178905,-9.511689,-10.134627 +101179198.236,101.179198,-9.913195,-10.131199 +101179491.205,101.179491,-9.745227,-10.127207 +101179784.173,101.179784,-9.752906,-10.118457 +101180077.142,101.180077,-9.831961,-10.137355 +101180370.110,101.180370,-10.135342,-10.157696 +101180663.079,101.180663,-10.185926,-10.179166 +101180956.047,101.180956,-10.084138,-10.201685 +101181249.016,101.181249,-9.659603,-10.224178 +101181541.984,101.181542,-9.338485,-10.247775 +101181834.953,101.181835,-9.335293,-10.274551 +101182127.921,101.182128,-9.376854,-10.301817 +101182420.890,101.182421,-9.630721,-10.327593 +101182713.858,101.182714,-9.790440,-10.352460 +101183006.827,101.183007,-9.365981,-10.377180 +101183299.795,101.183300,-9.131402,-10.402872 +101183592.764,101.183593,-9.121538,-10.432906 +101183885.732,101.183886,-8.971244,-10.462647 +101184178.701,101.184179,-9.524323,-10.490487 +101184471.669,101.184472,-9.885583,-10.520654 +101184764.638,101.184765,-9.721277,-10.552405 +101185057.606,101.185058,-9.755481,-10.584717 +101185350.575,101.185351,-9.435456,-10.617838 +101185643.543,101.185644,-9.279970,-10.649557 +101185936.512,101.185937,-9.212166,-10.682075 +101186229.480,101.186229,-9.175524,-10.713350 +101186522.449,101.186522,-9.052203,-10.744608 +101186815.417,101.186815,-9.133927,-10.775438 +101187108.386,101.187108,-8.927301,-10.808225 +101187401.354,101.187401,-9.171828,-10.841841 +101187694.323,101.187694,-9.793662,-10.877651 +101187987.291,101.187987,-10.218248,-10.917353 +101188280.260,101.188280,-10.311679,-10.957743 +101188573.228,101.188573,-10.265933,-10.997241 +101188866.197,101.188866,-10.150767,-11.033381 +101189159.165,101.189159,-9.953006,-11.072968 +101189452.134,101.189452,-9.987524,-11.114895 +101189745.102,101.189745,-9.507010,-11.158093 +101190038.071,101.190038,-9.488955,-11.199668 +101190331.039,101.190331,-10.091798,-11.241289 +101190624.008,101.190624,-10.349831,-11.289940 +101190916.976,101.190917,-10.080345,-11.339793 +101191209.945,101.191210,-10.231231,-11.389143 +101191502.913,101.191503,-10.028835,-11.436884 +101191795.882,101.191796,-10.594893,-11.482917 +101192088.850,101.192089,-10.650437,-11.532476 +101192381.819,101.192382,-9.996994,-11.580553 +101192674.787,101.192675,-9.901995,-11.626050 +101192967.756,101.192968,-10.418047,-11.671759 +101193260.724,101.193261,-10.168134,-11.714957 +101193553.693,101.193554,-9.889534,-11.759062 +101193846.661,101.193847,-9.880927,-11.806261 +101194139.630,101.194140,-10.442546,-11.857805 +101194432.598,101.194433,-10.907962,-11.906215 +101194725.567,101.194726,-10.467127,-11.945403 +101195018.535,101.195019,-10.368462,-11.986531 +101195311.504,101.195312,-10.315670,-12.037105 +101195604.472,101.195604,-10.161003,-12.091254 +101195897.441,101.195897,-9.933814,-12.143558 +101196190.409,101.196190,-10.691422,-12.195720 +101196483.378,101.196483,-10.740360,-12.248685 +101196776.346,101.196776,-10.799052,-12.306373 +101197069.315,101.197069,-10.580458,-12.368900 +101197362.283,101.197362,-10.322290,-12.430295 +101197655.252,101.197655,-10.642014,-12.487945 +101197948.220,101.197948,-10.360533,-12.544511 +101198241.189,101.198241,-10.196782,-12.600499 +101198534.157,101.198534,-10.326682,-12.658919 +101198827.126,101.198827,-10.362778,-12.724617 +101199120.094,101.199120,-11.168687,-12.791559 +101199413.063,101.199413,-10.851517,-12.853502 +101199706.031,101.199706,-10.531439,-12.918838 diff --git a/Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.png b/Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.png new file mode 100644 index 0000000..e6f18f8 Binary files /dev/null and b/Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.png differ diff --git a/Projects/SDR_Rover/data/raw/lab007_source.jpg b/Projects/SDR_Rover/data/raw/lab007_source.jpg new file mode 100644 index 0000000..1c96e91 Binary files /dev/null and b/Projects/SDR_Rover/data/raw/lab007_source.jpg differ diff --git a/Projects/SDR_Rover/data/raw/lab009_source.jpg b/Projects/SDR_Rover/data/raw/lab009_source.jpg new file mode 100644 index 0000000..e6664e4 Binary files /dev/null and b/Projects/SDR_Rover/data/raw/lab009_source.jpg differ diff --git a/Projects/SDR_Rover/data/raw/lab011_scene.jpg b/Projects/SDR_Rover/data/raw/lab011_scene.jpg new file mode 100644 index 0000000..d02855b Binary files /dev/null and b/Projects/SDR_Rover/data/raw/lab011_scene.jpg differ diff --git a/Projects/SDR_Rover/data/raw/lab012_safety_scene.jpg b/Projects/SDR_Rover/data/raw/lab012_safety_scene.jpg new file mode 100644 index 0000000..5773e41 Binary files /dev/null and b/Projects/SDR_Rover/data/raw/lab012_safety_scene.jpg differ diff --git a/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.json b/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.json new file mode 100644 index 0000000..60ff0a4 --- /dev/null +++ b/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.json @@ -0,0 +1,13 @@ +{ + "pluto_uri": "ip:192.168.2.1", + "center_frequency_hz": 100000000, + "sample_rate_hz": 2399998, + "rx_bandwidth_hz": 2000000, + "gain_control_mode": "slow_attack", + "sample_count": 262144, + "sample_dtype": "complex64", + "mean_i": 0.010410308837890625, + "mean_q": -0.01348876953125, + "rms": 506.9608154296875, + "peak": 1436.5299072265625 +} \ No newline at end of file diff --git a/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.npy b/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.npy new file mode 100644 index 0000000..c5cf4af Binary files /dev/null and b/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.npy differ diff --git a/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz_spectrum.png b/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz_spectrum.png new file mode 100644 index 0000000..086b821 Binary files /dev/null and b/Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz_spectrum.png differ diff --git a/Projects/SDR_Rover/data/received/lab007_reassembled.jpg b/Projects/SDR_Rover/data/received/lab007_reassembled.jpg new file mode 100644 index 0000000..1c96e91 Binary files /dev/null and b/Projects/SDR_Rover/data/received/lab007_reassembled.jpg differ diff --git a/Projects/SDR_Rover/data/received/lab008_received.jpg b/Projects/SDR_Rover/data/received/lab008_received.jpg new file mode 100644 index 0000000..1c96e91 Binary files /dev/null and b/Projects/SDR_Rover/data/received/lab008_received.jpg differ diff --git a/Projects/SDR_Rover/docs/.gitkeep b/Projects/SDR_Rover/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/gnuradio/.gitkeep b/Projects/SDR_Rover/gnuradio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/ground_station/.gitkeep b/Projects/SDR_Rover/ground_station/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/protocol/.gitkeep b/Projects/SDR_Rover/protocol/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/protocol/__init__.py b/Projects/SDR_Rover/protocol/__init__.py new file mode 100644 index 0000000..a3cd43e --- /dev/null +++ b/Projects/SDR_Rover/protocol/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 10 17:11:41 2026 + +@author: user +""" + diff --git a/Projects/SDR_Rover/protocol/image_fragments.py b/Projects/SDR_Rover/protocol/image_fragments.py new file mode 100644 index 0000000..5c7ead9 --- /dev/null +++ b/Projects/SDR_Rover/protocol/image_fragments.py @@ -0,0 +1,354 @@ +""" +Фрагментация и сборка изображений SDR Rover Link. + +Изображение рассматривается как обычная последовательность байтов. +Модулю неважно, используется JPEG, PNG или другой формат. +""" + +from dataclasses import dataclass +import struct +from typing import Iterable + +from protocol.packet import MAX_PAYLOAD_SIZE + + +# ============================================================ +# Формат заголовка фрагмента изображения +# ============================================================ + +# I — идентификатор изображения, 4 байта +# H — номер фрагмента, 2 байта +# H — общее количество фрагментов, 2 байта +# I — размер исходного изображения, 4 байта +FRAGMENT_HEADER_FORMAT = ">IHHI" + +FRAGMENT_HEADER_SIZE = struct.calcsize( + FRAGMENT_HEADER_FORMAT +) + +MAX_FRAGMENT_DATA_SIZE = ( + MAX_PAYLOAD_SIZE - FRAGMENT_HEADER_SIZE +) + + +# ============================================================ +# Ошибки +# ============================================================ + +class ImageFragmentError(ValueError): + """Ошибка структуры фрагмента изображения.""" + + +class MissingFragmentsError(ImageFragmentError): + """Отсутствуют необходимые фрагменты изображения.""" + + +# ============================================================ +# Представление одного фрагмента +# ============================================================ + +@dataclass(frozen=True) +class ImageFragment: + """ + Один фрагмент изображения. + + Атрибуты + -------- + image_id: + Идентификатор изображения. + + fragment_index: + Номер фрагмента. Нумерация начинается с нуля. + + fragment_count: + Общее количество фрагментов. + + original_size: + Размер исходного изображения в байтах. + + data: + Часть исходного изображения. + """ + + image_id: int + fragment_index: int + fragment_count: int + original_size: int + data: bytes + + +# ============================================================ +# Разбиение изображения +# ============================================================ + +def split_image_bytes( + image_bytes: bytes, + image_id: int, + fragment_data_size: int = 512, +) -> list[ImageFragment]: + """ + Разбить изображение на фрагменты. + + Параметры + -------- + image_bytes: + Полное содержимое файла изображения. + + image_id: + Идентификатор изображения от 0 до 4294967295. + + fragment_data_size: + Максимальный размер данных одного фрагмента. + + Возвращает + ---------- + Список объектов ImageFragment. + """ + + if not isinstance(image_bytes, (bytes, bytearray)): + raise TypeError( + "image_bytes должен иметь тип bytes или bytearray" + ) + + image_bytes = bytes(image_bytes) + + if not image_bytes: + raise ValueError( + "Изображение не должно быть пустым" + ) + + if not 0 <= image_id <= 0xFFFFFFFF: + raise ValueError( + "image_id должен находиться " + "в диапазоне 0...4294967295" + ) + + if not 1 <= fragment_data_size <= MAX_FRAGMENT_DATA_SIZE: + raise ValueError( + "Некорректный размер фрагмента" + ) + + fragment_count = ( + len(image_bytes) + + fragment_data_size + - 1 + ) // fragment_data_size + + if fragment_count > 65535: + raise ValueError( + "Изображение требует более 65535 фрагментов" + ) + + fragments = [] + + for fragment_index in range(fragment_count): + + start = fragment_index * fragment_data_size + end = start + fragment_data_size + + fragment_data = image_bytes[start:end] + + fragments.append( + ImageFragment( + image_id=image_id, + fragment_index=fragment_index, + fragment_count=fragment_count, + original_size=len(image_bytes), + data=fragment_data, + ) + ) + + return fragments + + +# ============================================================ +# Преобразование фрагмента в байты +# ============================================================ + +def encode_image_fragment( + fragment: ImageFragment, +) -> bytes: + """ + Преобразовать ImageFragment в полезную нагрузку пакета. + """ + + if not isinstance(fragment, ImageFragment): + raise TypeError( + "fragment должен иметь тип ImageFragment" + ) + + if not 0 <= fragment.fragment_index < fragment.fragment_count: + raise ImageFragmentError( + "Некорректный номер фрагмента" + ) + + if not fragment.data: + raise ImageFragmentError( + "Фрагмент не должен быть пустым" + ) + + if len(fragment.data) > MAX_FRAGMENT_DATA_SIZE: + raise ImageFragmentError( + "Данные фрагмента слишком велики" + ) + + header = struct.pack( + FRAGMENT_HEADER_FORMAT, + fragment.image_id, + fragment.fragment_index, + fragment.fragment_count, + fragment.original_size, + ) + + return header + fragment.data + + +# ============================================================ +# Преобразование байтов обратно во фрагмент +# ============================================================ + +def decode_image_fragment( + payload: bytes, +) -> ImageFragment: + """ + Разобрать полезную нагрузку пакета изображения. + """ + + if not isinstance(payload, (bytes, bytearray)): + raise TypeError( + "payload должен иметь тип bytes или bytearray" + ) + + payload = bytes(payload) + + if len(payload) <= FRAGMENT_HEADER_SIZE: + raise ImageFragmentError( + "Фрагмент слишком короткий" + ) + + header = payload[:FRAGMENT_HEADER_SIZE] + fragment_data = payload[FRAGMENT_HEADER_SIZE:] + + ( + image_id, + fragment_index, + fragment_count, + original_size, + ) = struct.unpack( + FRAGMENT_HEADER_FORMAT, + header, + ) + + if fragment_count == 0: + raise ImageFragmentError( + "Количество фрагментов не может быть равно нулю" + ) + + if fragment_index >= fragment_count: + raise ImageFragmentError( + "Номер фрагмента выходит за допустимый диапазон" + ) + + if original_size == 0: + raise ImageFragmentError( + "Размер исходного изображения равен нулю" + ) + + return ImageFragment( + image_id=image_id, + fragment_index=fragment_index, + fragment_count=fragment_count, + original_size=original_size, + data=fragment_data, + ) + + +# ============================================================ +# Сборка изображения +# ============================================================ + +def reassemble_image( + fragments: Iterable[ImageFragment], +) -> bytes: + """ + Собрать исходное изображение из фрагментов. + + Фрагменты могут поступать в произвольном порядке. + """ + + fragments = list(fragments) + + if not fragments: + raise MissingFragmentsError( + "Список фрагментов пуст" + ) + + first_fragment = fragments[0] + + image_id = first_fragment.image_id + fragment_count = first_fragment.fragment_count + original_size = first_fragment.original_size + + fragments_by_index = {} + + for fragment in fragments: + + if fragment.image_id != image_id: + raise ImageFragmentError( + "Фрагменты относятся к разным изображениям" + ) + + if fragment.fragment_count != fragment_count: + raise ImageFragmentError( + "Различается общее количество фрагментов" + ) + + if fragment.original_size != original_size: + raise ImageFragmentError( + "Различается размер исходного изображения" + ) + + existing_fragment = fragments_by_index.get( + fragment.fragment_index + ) + + if existing_fragment is not None: + + if existing_fragment.data != fragment.data: + raise ImageFragmentError( + "Получены разные данные " + "для одного номера фрагмента" + ) + + # Полностью одинаковый повторный фрагмент + # можно безопасно проигнорировать. + continue + + fragments_by_index[ + fragment.fragment_index + ] = fragment + + missing_indexes = [ + index + for index in range(fragment_count) + if index not in fragments_by_index + ] + + if missing_indexes: + raise MissingFragmentsError( + "Отсутствуют фрагменты: " + + ", ".join(map(str, missing_indexes)) + ) + + assembled_bytes = b"".join( + fragments_by_index[index].data + for index in range(fragment_count) + ) + + if len(assembled_bytes) != original_size: + raise ImageFragmentError( + "Размер собранного изображения " + "не совпадает с исходным размером" + ) + + return assembled_bytes \ No newline at end of file diff --git a/Projects/SDR_Rover/protocol/link_adaptation.py b/Projects/SDR_Rover/protocol/link_adaptation.py new file mode 100644 index 0000000..42ab0c3 --- /dev/null +++ b/Projects/SDR_Rover/protocol/link_adaptation.py @@ -0,0 +1,382 @@ +""" +Адаптация параметров SDR Rover Link к качеству канала. + +Модуль оценивает несколько размеров фрагмента изображения +и выбирает режим с максимальной полезной скоростью. + +Используемая модель: + +- когерентная BPSK; +- канал AWGN; +- независимые битовые ошибки; +- CRC отбрасывает повреждённые пакеты; +- Stop-and-Wait ARQ; +- одинаковое Eb/N0 для DATA и ACK. +""" + +from dataclasses import dataclass +from math import ( + erfc, + exp, + log1p, + sqrt, +) + +from protocol.image_fragments import ( + FRAGMENT_HEADER_SIZE, +) + +from protocol.packet import ( + CRC_SIZE, + HEADER_SIZE, +) + + +# ============================================================ +# Результат оценки одного размера фрагмента +# ============================================================ + +@dataclass(frozen=True) +class FragmentModeEstimate: + """ + Оценка одного возможного размера фрагмента. + """ + + fragment_size: int + + ber: float + + data_packet_size_bytes: int + ack_packet_size_bytes: int + + data_success_probability: float + ack_success_probability: float + + confirmed_attempt_probability: float + + expected_attempts: float + + success_probability_with_retries: float + + expected_goodput_bps: float + + +# ============================================================ +# Итоговое решение адаптера +# ============================================================ + +@dataclass(frozen=True) +class LinkDecision: + """ + Решение о режиме передачи изображения. + """ + + eb_n0_db: float + + images_enabled: bool + + selected_fragment_size: int | None + + reason: str + + estimates: tuple[FragmentModeEstimate, ...] + + +# ============================================================ +# Теоретический BER +# ============================================================ + +def theoretical_bpsk_ber( + eb_n0_db: float, +) -> float: + """ + Теоретический BER когерентной BPSK в AWGN. + + BER = 0.5 * erfc(sqrt(Eb/N0)) + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + return 0.5 * erfc( + sqrt(eb_n0_linear) + ) + + +# ============================================================ +# Вероятность передачи пакета без ошибок +# ============================================================ + +def packet_success_probability( + ber: float, + packet_bit_count: int, +) -> float: + """ + Рассчитать вероятность того, что пакет не содержит + ни одной битовой ошибки. + + P = (1 - BER) ** N + """ + + if not 0.0 <= ber <= 1.0: + raise ValueError( + "BER должен находиться в диапазоне 0...1" + ) + + if packet_bit_count <= 0: + raise ValueError( + "packet_bit_count должен быть положительным" + ) + + if ber == 0.0: + return 1.0 + + if ber == 1.0: + return 0.0 + + return exp( + packet_bit_count + * log1p(-ber) + ) + + +# ============================================================ +# Оценка отдельного режима +# ============================================================ + +def evaluate_fragment_mode( + eb_n0_db: float, + fragment_size: int, + channel_bitrate_bps: int, + max_attempts: int, +) -> FragmentModeEstimate: + """ + Оценить передачу полного фрагмента заданного размера. + """ + + if fragment_size <= 0: + raise ValueError( + "fragment_size должен быть положительным" + ) + + if channel_bitrate_bps <= 0: + raise ValueError( + "channel_bitrate_bps должен быть положительным" + ) + + if max_attempts <= 0: + raise ValueError( + "max_attempts должен быть положительным" + ) + + ber = theoretical_bpsk_ber( + eb_n0_db + ) + + # DATA содержит: + # + # заголовок протокола; + # заголовок фрагмента изображения; + # данные JPEG; + # CRC-32. + data_packet_size_bytes = ( + HEADER_SIZE + + FRAGMENT_HEADER_SIZE + + fragment_size + + CRC_SIZE + ) + + # ACK содержит только заголовок протокола и CRC. + ack_packet_size_bytes = ( + HEADER_SIZE + + CRC_SIZE + ) + + data_success_probability = ( + packet_success_probability( + ber=ber, + packet_bit_count=( + data_packet_size_bytes * 8 + ), + ) + ) + + ack_success_probability = ( + packet_success_probability( + ber=ber, + packet_bit_count=( + ack_packet_size_bytes * 8 + ), + ) + ) + + # Одна попытка считается завершённой, + # только если правильно прошли DATA и ACK. + confirmed_attempt_probability = ( + data_success_probability + * ack_success_probability + ) + + if confirmed_attempt_probability == 0.0: + expected_attempts = float("inf") + expected_goodput_bps = 0.0 + + else: + expected_attempts = ( + 1.0 + / confirmed_attempt_probability + ) + + # Ожидаемый объём DATA до подтверждения. + expected_data_bytes = ( + data_packet_size_bytes + / confirmed_attempt_probability + ) + + # ACK отправляется только после исправного DATA. + expected_ack_bytes = ( + ack_packet_size_bytes + / ack_success_probability + ) + + expected_total_bytes = ( + expected_data_bytes + + expected_ack_bytes + ) + + expected_goodput_bps = ( + channel_bitrate_bps + * fragment_size + / expected_total_bytes + ) + + success_probability_with_retries = ( + 1.0 + - ( + 1.0 + - confirmed_attempt_probability + ) ** max_attempts + ) + + return FragmentModeEstimate( + fragment_size=fragment_size, + ber=ber, + data_packet_size_bytes=data_packet_size_bytes, + ack_packet_size_bytes=ack_packet_size_bytes, + data_success_probability=( + data_success_probability + ), + ack_success_probability=( + ack_success_probability + ), + confirmed_attempt_probability=( + confirmed_attempt_probability + ), + expected_attempts=expected_attempts, + success_probability_with_retries=( + success_probability_with_retries + ), + expected_goodput_bps=( + expected_goodput_bps + ), + ) + + +# ============================================================ +# Выбор режима +# ============================================================ + +def choose_image_mode( + eb_n0_db: float, + candidate_fragment_sizes: tuple[int, ...] = ( + 128, + 512, + 1024, + ), + channel_bitrate_bps: int = 20_000, + max_attempts: int = 5, + minimum_success_probability: float = 0.85, + minimum_goodput_bps: float = 2_000.0, +) -> LinkDecision: + """ + Выбрать режим передачи изображения. + + Режим допускается, если: + + 1. вероятность подтверждения за max_attempts + не ниже minimum_success_probability; + + 2. ожидаемый goodput не ниже minimum_goodput_bps. + + Из подходящих режимов выбирается режим + с максимальной полезной скоростью. + """ + + if not candidate_fragment_sizes: + raise ValueError( + "Список размеров фрагментов пуст" + ) + + if not 0.0 <= minimum_success_probability <= 1.0: + raise ValueError( + "minimum_success_probability должен быть 0...1" + ) + + if minimum_goodput_bps < 0.0: + raise ValueError( + "minimum_goodput_bps не может быть отрицательным" + ) + + estimates = tuple( + evaluate_fragment_mode( + eb_n0_db=eb_n0_db, + fragment_size=fragment_size, + channel_bitrate_bps=channel_bitrate_bps, + max_attempts=max_attempts, + ) + for fragment_size in candidate_fragment_sizes + ) + + eligible_estimates = [ + estimate + for estimate in estimates + if ( + estimate.success_probability_with_retries + >= minimum_success_probability + and estimate.expected_goodput_bps + >= minimum_goodput_bps + ) + ] + + if not eligible_estimates: + return LinkDecision( + eb_n0_db=eb_n0_db, + images_enabled=False, + selected_fragment_size=None, + reason=( + "Канал недостаточен для надёжной " + "передачи изображения" + ), + estimates=estimates, + ) + + selected_estimate = max( + eligible_estimates, + key=lambda estimate: ( + estimate.expected_goodput_bps + ), + ) + + return LinkDecision( + eb_n0_db=eb_n0_db, + images_enabled=True, + selected_fragment_size=( + selected_estimate.fragment_size + ), + reason=( + "Выбран режим с максимальной " + "ожидаемой полезной скоростью" + ), + estimates=estimates, + ) \ No newline at end of file diff --git a/Projects/SDR_Rover/protocol/link_controller.py b/Projects/SDR_Rover/protocol/link_controller.py new file mode 100644 index 0000000..96ff701 --- /dev/null +++ b/Projects/SDR_Rover/protocol/link_controller.py @@ -0,0 +1,447 @@ +""" +Устойчивый контроллер режима радиоканала SDR Rover Link. + +Контроллер устраняет частое переключение режимов +из-за шума измерения Eb/N0. + +Реализованы: + +1. Экспоненциальное усреднение Eb/N0. +2. Гистерезис порогов. +3. Подтверждение повышения режима. +4. Минимальное время удержания режима. +5. Быстрое аварийное понижение режима. + +Доступные режимы: + + 0 — изображения отключены; + 128 — фрагменты по 128 байт; + 512 — фрагменты по 512 байт; + 1024 — фрагменты по 1024 байта. +""" + +from dataclasses import dataclass + + +# ============================================================ +# Допустимые режимы +# ============================================================ + +IMAGE_OFF = 0 + +FRAGMENT_MODE_128 = 128 +FRAGMENT_MODE_512 = 512 +FRAGMENT_MODE_1024 = 1024 + +VALID_MODES = ( + IMAGE_OFF, + FRAGMENT_MODE_128, + FRAGMENT_MODE_512, + FRAGMENT_MODE_1024, +) + + +# ============================================================ +# Результат одного шага контроллера +# ============================================================ + +@dataclass(frozen=True) +class LinkControllerResult: + """ + Состояние контроллера после нового измерения Eb/N0. + """ + + step: int + + measured_eb_n0_db: float + filtered_eb_n0_db: float + + previous_mode: int + selected_mode: int + + mode_changed: bool + + pending_upgrade_mode: int | None + pending_upgrade_count: int + + reason: str + + +# ============================================================ +# Контроллер +# ============================================================ + +class LinkModeController: + """ + Устойчивый адаптивный контроллер режима изображения. + + Пороговые значения выбраны по результатам Lab015 и Lab016. + + Гистерезис означает, что повышение и понижение режима + происходят при разных значениях Eb/N0. + + Пример для режима 512 байт: + + повышение 128 → 512: Eb/N0 >= 8,7 дБ + понижение 512 → 128: Eb/N0 < 8,1 дБ + + Благодаря разнице 0,6 дБ небольшие колебания около + порога не вызывают постоянного переключения. + """ + + def __init__( + self, + initial_mode: int = IMAGE_OFF, + filter_alpha: float = 0.35, + upgrade_confirmation_count: int = 3, + minimum_hold_steps: int = 2, + ) -> None: + """ + Создать контроллер. + + Параметры + ---------- + initial_mode: + Начальный режим. + + filter_alpha: + Коэффициент экспоненциального фильтра. + + Большое значение: + быстрее реакция; + слабее фильтрация шума. + + Малое значение: + медленнее реакция; + сильнее фильтрация. + + upgrade_confirmation_count: + Сколько последовательных измерений требуется + для повышения режима. + + minimum_hold_steps: + Минимальное число шагов удержания режима + перед очередным повышением. + """ + + if initial_mode not in VALID_MODES: + raise ValueError( + f"Недопустимый начальный режим: {initial_mode}" + ) + + if not 0.0 < filter_alpha <= 1.0: + raise ValueError( + "filter_alpha должен находиться в диапазоне 0...1" + ) + + if upgrade_confirmation_count <= 0: + raise ValueError( + "upgrade_confirmation_count должен быть положительным" + ) + + if minimum_hold_steps < 0: + raise ValueError( + "minimum_hold_steps не может быть отрицательным" + ) + + self.current_mode = initial_mode + + self.filter_alpha = filter_alpha + + self.upgrade_confirmation_count = ( + upgrade_confirmation_count + ) + + self.minimum_hold_steps = ( + minimum_hold_steps + ) + + self.filtered_eb_n0_db: float | None = None + + self.step = 0 + self.steps_in_mode = 0 + + self.pending_upgrade_mode: int | None = None + self.pending_upgrade_count = 0 + + # ======================================================== + # Внутренние функции + # ======================================================== + + def _update_filter( + self, + measured_eb_n0_db: float, + ) -> float: + """ + Обновить экспоненциальное среднее. + + Формула: + + filtered = + alpha * measured + + (1 - alpha) * previous_filtered + """ + + if self.filtered_eb_n0_db is None: + self.filtered_eb_n0_db = measured_eb_n0_db + + else: + self.filtered_eb_n0_db = ( + self.filter_alpha + * measured_eb_n0_db + + ( + 1.0 + - self.filter_alpha + ) + * self.filtered_eb_n0_db + ) + + return self.filtered_eb_n0_db + + def _emergency_safe_mode( + self, + measured_eb_n0_db: float, + ) -> int: + """ + Определить максимально безопасный режим + по мгновенному измерению. + + Используется для быстрого понижения режима, + не дожидаясь усреднения. + """ + + if measured_eb_n0_db < 5.8: + return IMAGE_OFF + + if measured_eb_n0_db < 7.7: + return FRAGMENT_MODE_128 + + if measured_eb_n0_db < 8.7: + return FRAGMENT_MODE_512 + + return FRAGMENT_MODE_1024 + + def _desired_mode_from_filtered( + self, + filtered_eb_n0_db: float, + ) -> int: + """ + Определить желаемый режим с учётом гистерезиса. + + Повышение выполняется только на один уровень за шаг. + """ + + if self.current_mode == IMAGE_OFF: + + if filtered_eb_n0_db >= 6.8: + return FRAGMENT_MODE_128 + + return IMAGE_OFF + + if self.current_mode == FRAGMENT_MODE_128: + + if filtered_eb_n0_db < 6.2: + return IMAGE_OFF + + if filtered_eb_n0_db >= 8.7: + return FRAGMENT_MODE_512 + + return FRAGMENT_MODE_128 + + if self.current_mode == FRAGMENT_MODE_512: + + if filtered_eb_n0_db < 8.1: + return FRAGMENT_MODE_128 + + if filtered_eb_n0_db >= 9.8: + return FRAGMENT_MODE_1024 + + return FRAGMENT_MODE_512 + + if self.current_mode == FRAGMENT_MODE_1024: + + if filtered_eb_n0_db < 9.2: + return FRAGMENT_MODE_512 + + return FRAGMENT_MODE_1024 + + raise RuntimeError( + f"Неизвестный текущий режим: {self.current_mode}" + ) + + def _reset_pending_upgrade( + self, + ) -> None: + """ + Сбросить накопленное подтверждение повышения. + """ + + self.pending_upgrade_mode = None + self.pending_upgrade_count = 0 + + def _change_mode( + self, + new_mode: int, + ) -> None: + """ + Немедленно установить новый режим. + """ + + if new_mode not in VALID_MODES: + raise ValueError( + f"Недопустимый режим: {new_mode}" + ) + + self.current_mode = new_mode + self.steps_in_mode = 0 + + self._reset_pending_upgrade() + + # ======================================================== + # Основной шаг + # ======================================================== + + def update( + self, + measured_eb_n0_db: float, + ) -> LinkControllerResult: + """ + Обработать новое измерение Eb/N0. + + Логика: + + 1. Обновить фильтр. + 2. Проверить аварийное понижение. + 3. Проверить обычное понижение по фильтру. + 4. Для повышения потребовать несколько подтверждений. + 5. Вернуть результат. + """ + + measured_eb_n0_db = float( + measured_eb_n0_db + ) + + previous_mode = self.current_mode + + filtered_eb_n0_db = self._update_filter( + measured_eb_n0_db + ) + + reason = "Режим удерживается" + + # ---------------------------------------------------- + # Аварийное быстрое понижение + # ---------------------------------------------------- + + emergency_mode = self._emergency_safe_mode( + measured_eb_n0_db + ) + + if emergency_mode < self.current_mode: + + self._change_mode( + emergency_mode + ) + + reason = ( + "Аварийное понижение по мгновенному Eb/N0" + ) + + else: + + desired_mode = self._desired_mode_from_filtered( + filtered_eb_n0_db + ) + + # ------------------------------------------------ + # Обычное понижение + # ------------------------------------------------ + + if desired_mode < self.current_mode: + + self._change_mode( + desired_mode + ) + + reason = ( + "Понижение по фильтрованному Eb/N0" + ) + + # ------------------------------------------------ + # Возможное повышение + # ------------------------------------------------ + + elif desired_mode > self.current_mode: + + if ( + self.steps_in_mode + < self.minimum_hold_steps + ): + self._reset_pending_upgrade() + + reason = ( + "Повышение запрещено: " + "не выдержано минимальное время режима" + ) + + else: + + if ( + self.pending_upgrade_mode + == desired_mode + ): + self.pending_upgrade_count += 1 + + else: + self.pending_upgrade_mode = ( + desired_mode + ) + + self.pending_upgrade_count = 1 + + reason = ( + "Накопление подтверждений " + "для повышения режима" + ) + + if ( + self.pending_upgrade_count + >= self.upgrade_confirmation_count + ): + self._change_mode( + desired_mode + ) + + reason = ( + "Режим повышен после " + "нескольких подтверждений" + ) + + else: + self._reset_pending_upgrade() + + mode_changed = ( + self.current_mode != previous_mode + ) + + result = LinkControllerResult( + step=self.step, + measured_eb_n0_db=measured_eb_n0_db, + filtered_eb_n0_db=filtered_eb_n0_db, + previous_mode=previous_mode, + selected_mode=self.current_mode, + mode_changed=mode_changed, + pending_upgrade_mode=( + self.pending_upgrade_mode + ), + pending_upgrade_count=( + self.pending_upgrade_count + ), + reason=reason, + ) + + self.step += 1 + self.steps_in_mode += 1 + + return result + \ No newline at end of file diff --git a/Projects/SDR_Rover/protocol/packet.py b/Projects/SDR_Rover/protocol/packet.py new file mode 100644 index 0000000..9156b1a --- /dev/null +++ b/Projects/SDR_Rover/protocol/packet.py @@ -0,0 +1,254 @@ +""" +Пакетный протокол SDR Rover Link. + +Модуль содержит функции: +- build_packet() — формирование пакета; +- parse_packet() — проверка и разбор пакета. + +Версия протокола: 0.1 +""" + +from dataclasses import dataclass +import struct +import zlib + + +# ============================================================ +# Константы протокола +# ============================================================ + +SYNC_WORD = 0xAA55 +PROTOCOL_VERSION = 1 + +MESSAGE_TYPE_TEXT = 1 +MESSAGE_TYPE_TELEMETRY = 2 +MESSAGE_TYPE_IMAGE_FRAGMENT = 3 +MESSAGE_TYPE_ACK = 4 + +# Структура заголовка: +# +# H — SYNC, 2 байта +# B — версия, 1 байт +# B — тип сообщения, 1 байт +# H — порядковый номер, 2 байта +# H — длина полезной нагрузки, 2 байта +HEADER_FORMAT = ">HBBHH" + +# CRC-32 хранится как беззнаковое 32-битное число. +CRC_FORMAT = ">I" + +HEADER_SIZE = struct.calcsize(HEADER_FORMAT) +CRC_SIZE = struct.calcsize(CRC_FORMAT) + +MAX_PAYLOAD_SIZE = 65535 + + +# ============================================================ +# Ошибки протокола +# ============================================================ + +class PacketError(ValueError): + """Общая ошибка структуры пакета.""" + + +class CRCError(PacketError): + """Ошибка контрольной суммы CRC.""" + + +# ============================================================ +# Результат разбора пакета +# ============================================================ + +@dataclass(frozen=True) +class ParsedPacket: + """ + Представление успешно принятого пакета. + + Атрибуты: + version: версия протокола; + message_type: тип сообщения; + sequence_number: порядковый номер; + payload: полезная нагрузка в байтах. + """ + + version: int + message_type: int + sequence_number: int + payload: bytes + + +# ============================================================ +# Формирование пакета +# ============================================================ + +def build_packet( + payload: bytes, + message_type: int, + sequence_number: int, +) -> bytes: + """ + Сформировать пакет SDR Rover Link. + + Параметры: + payload: + Полезная нагрузка в байтах. + + message_type: + Тип сообщения от 0 до 255. + + sequence_number: + Порядковый номер от 0 до 65535. + + Возвращает: + Полный пакет в виде bytes. + """ + + if not isinstance(payload, (bytes, bytearray)): + raise TypeError( + "payload должен иметь тип bytes или bytearray" + ) + + payload = bytes(payload) + + if not 0 <= message_type <= 255: + raise ValueError( + "message_type должен находиться в диапазоне 0...255" + ) + + if not 0 <= sequence_number <= 65535: + raise ValueError( + "sequence_number должен находиться в диапазоне 0...65535" + ) + + payload_length = len(payload) + + if payload_length > MAX_PAYLOAD_SIZE: + raise ValueError( + f"Полезная нагрузка превышает {MAX_PAYLOAD_SIZE} байт" + ) + + header = struct.pack( + HEADER_FORMAT, + SYNC_WORD, + PROTOCOL_VERSION, + message_type, + sequence_number, + payload_length, + ) + + packet_without_crc = header + payload + + crc_value = ( + zlib.crc32(packet_without_crc) & 0xFFFFFFFF + ) + + crc_bytes = struct.pack( + CRC_FORMAT, + crc_value, + ) + + return packet_without_crc + crc_bytes + + +# ============================================================ +# Разбор пакета +# ============================================================ + +def parse_packet(packet: bytes) -> ParsedPacket: + """ + Проверить и разобрать пакет SDR Rover Link. + + Проверяются: + - минимальный размер; + - маркер SYNC; + - версия протокола; + - длина пакета; + - CRC-32. + + Возвращает: + ParsedPacket. + + Вызывает: + PacketError — при ошибке структуры; + CRCError — при несовпадении CRC. + """ + + if not isinstance(packet, (bytes, bytearray)): + raise TypeError( + "packet должен иметь тип bytes или bytearray" + ) + + packet = bytes(packet) + + minimum_packet_size = HEADER_SIZE + CRC_SIZE + + if len(packet) < minimum_packet_size: + raise PacketError( + "Пакет слишком короткий" + ) + + header = packet[:HEADER_SIZE] + + ( + sync_word, + version, + message_type, + sequence_number, + payload_length, + ) = struct.unpack( + HEADER_FORMAT, + header, + ) + + if sync_word != SYNC_WORD: + raise PacketError( + f"Неверный SYNC: 0x{sync_word:04X}" + ) + + if version != PROTOCOL_VERSION: + raise PacketError( + f"Неподдерживаемая версия протокола: {version}" + ) + + expected_packet_size = ( + HEADER_SIZE + + payload_length + + CRC_SIZE + ) + + if len(packet) != expected_packet_size: + raise PacketError( + "Длина пакета не соответствует полю LENGTH: " + f"ожидалось {expected_packet_size} байт, " + f"получено {len(packet)} байт" + ) + + data_without_crc = packet[:-CRC_SIZE] + received_crc_bytes = packet[-CRC_SIZE:] + + (received_crc,) = struct.unpack( + CRC_FORMAT, + received_crc_bytes, + ) + + calculated_crc = ( + zlib.crc32(data_without_crc) & 0xFFFFFFFF + ) + + if received_crc != calculated_crc: + raise CRCError( + "CRC не совпадает: " + f"получено 0x{received_crc:08X}, " + f"вычислено 0x{calculated_crc:08X}" + ) + + payload = packet[ + HEADER_SIZE:-CRC_SIZE + ] + + return ParsedPacket( + version=version, + message_type=message_type, + sequence_number=sequence_number, + payload=payload, + ) \ No newline at end of file diff --git a/Projects/SDR_Rover/rover/.gitkeep b/Projects/SDR_Rover/rover/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/tests/.gitkeep b/Projects/SDR_Rover/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/tests/__init__.py b/Projects/SDR_Rover/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/tests/lab001_text_bytes.py b/Projects/SDR_Rover/tests/lab001_text_bytes.py new file mode 100644 index 0000000..d40a639 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab001_text_bytes.py @@ -0,0 +1,54 @@ +""" +Lab001. Преобразование текста в байты и биты. + +Цель: +1. Взять обычную текстовую строку. +2. Преобразовать её в байты UTF-8. +3. Показать каждый байт как десятичное число. +4. Показать каждый байт как восемь бит. +5. Восстановить исходный текст. +""" + +# Исходное сообщение +message = "HELLO SDR" + +# Кодирование текста в байты. +# UTF-8 — способ представить символы числами. +encoded_message = message.encode("utf-8") + +# Преобразование каждого байта в строку из восьми бит. +binary_message = " ".join( + f"{byte:08b}" for byte in encoded_message +) + +# Обратное преобразование байтов в текст. +decoded_message = encoded_message.decode("utf-8") + + +print("Исходный текст:") +print(message) + +print("\nОбъект bytes:") +print(encoded_message) + +print("\nБайты в десятичном виде:") +print(list(encoded_message)) + +print("\nБайты в двоичном виде:") +print(binary_message) + +print("\nПодробно по каждому символу:") +for character, byte_value in zip(message, encoded_message): + print( + f"{character!r:>4} " + f"→ число {byte_value:3d} " + f"→ биты {byte_value:08b}" + ) + +print("\nВосстановленный текст:") +print(decoded_message) + +# Автоматическая проверка результата. +assert decoded_message == message + +print("\nПроверка пройдена: исходный текст восстановлен без ошибок.") \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab001_utf8_russian.py b/Projects/SDR_Rover/tests/lab001_utf8_russian.py new file mode 100644 index 0000000..a3722d2 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab001_utf8_russian.py @@ -0,0 +1,61 @@ +""" +Lab001, часть 2. +Русский текст, символы, байты и кодировка UTF-8. + +Цель: +1. Увидеть разницу между количеством символов и количеством байтов. +2. Посмотреть, сколько байтов занимает каждая русская буква. +3. Восстановить исходный текст из байтов. +""" + +# Исходное сообщение +message = "ПРИВЕТ SDR" + +# Преобразование текста в байты UTF-8 +encoded_message = message.encode("utf-8") + +# Обратное преобразование байтов в текст +decoded_message = encoded_message.decode("utf-8") + + +print("Исходный текст:") +print(message) + +print("\nКоличество символов:") +print(len(message)) + +print("\nКоличество байтов UTF-8:") +print(len(encoded_message)) + +print("\nОбъект bytes:") +print(encoded_message) + +print("\nВсе байты в десятичном виде:") +print(list(encoded_message)) + +print("\nПодробно по каждому символу:") + +for character in message: + character_bytes = character.encode("utf-8") + + decimal_bytes = list(character_bytes) + + binary_bytes = " ".join( + f"{byte:08b}" for byte in character_bytes + ) + + print( + f"{character!r:>4} " + f"→ байтов: {len(character_bytes)} " + f"→ числа: {decimal_bytes} " + f"→ биты: {binary_bytes}" + ) + +print("\nВосстановленный текст:") +print(decoded_message) + +# Автоматические проверки +assert decoded_message == message +assert len(encoded_message) >= len(message) + +print("\nПроверка пройдена: русский текст восстановлен без ошибок.") \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab002_bit_corruption.py b/Projects/SDR_Rover/tests/lab002_bit_corruption.py new file mode 100644 index 0000000..057fed9 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab002_bit_corruption.py @@ -0,0 +1,153 @@ +""" +Lab002, часть 2. +Повреждение одного бита пакета без контроля целостности. + +Цель: +1. Сформировать цифровой пакет. +2. Намеренно изменить один бит полезной нагрузки. +3. Разобрать повреждённый пакет. +4. Убедиться, что без CRC повреждение не обнаруживается. +""" + +import struct + + +# ============================================================ +# Константы протокола +# ============================================================ + +SYNC_WORD = 0xAA55 +PROTOCOL_VERSION = 1 +MESSAGE_TYPE_TEXT = 1 + +HEADER_FORMAT = ">HBBHH" + + +# ============================================================ +# Формирование исходного пакета +# ============================================================ + +message = "ПРИВЕТ SDR" +payload = message.encode("utf-8") + +sequence_number = 1 +message_type = MESSAGE_TYPE_TEXT +payload_length = len(payload) + +header = struct.pack( + HEADER_FORMAT, + SYNC_WORD, + PROTOCOL_VERSION, + message_type, + sequence_number, + payload_length, +) + +packet = header + payload + + +print("Исходное сообщение:") +print(message) + +print("\nИсходный пакет:") +print(packet.hex(" ")) + + +# ============================================================ +# Имитация повреждения пакета +# ============================================================ + +# bytes нельзя изменять напрямую. +# Поэтому преобразуем пакет в изменяемый массив bytearray. +corrupted_packet = bytearray(packet) + +header_size = struct.calcsize(HEADER_FORMAT) + +# В строке "ПРИВЕТ SDR": +# +# "ПРИВЕТ" занимает 12 байтов UTF-8, +# пробел занимает 1 байт, +# буква S находится по индексу 13 внутри PAYLOAD. +payload_byte_index = 13 + +# Полный индекс внутри пакета: +packet_byte_index = header_size + payload_byte_index + +original_byte = corrupted_packet[packet_byte_index] + +# XOR с 0x01 изменяет младший бит: +# +# 0x53 = 01010011 = S +# 0x52 = 01010010 = R +corrupted_packet[packet_byte_index] ^= 0x01 + +corrupted_byte = corrupted_packet[packet_byte_index] + + +print("\nИзменяемый байт внутри пакета:") + +print( + f"До повреждения: " + f"{original_byte:3d} " + f"= 0x{original_byte:02X} " + f"= {original_byte:08b}" +) + +print( + f"После повреждения: " + f"{corrupted_byte:3d} " + f"= 0x{corrupted_byte:02X} " + f"= {corrupted_byte:08b}" +) + +print("\nПовреждённый пакет:") +print(bytes(corrupted_packet).hex(" ")) + + +# ============================================================ +# Разбор повреждённого пакета +# ============================================================ + +received_header = corrupted_packet[:header_size] +received_payload = corrupted_packet[header_size:] + +( + received_sync, + received_version, + received_type, + received_sequence, + received_length, +) = struct.unpack(HEADER_FORMAT, received_header) + + +# ============================================================ +# Проверки структуры +# ============================================================ + +assert received_sync == SYNC_WORD +assert received_version == PROTOCOL_VERSION +assert received_type == MESSAGE_TYPE_TEXT +assert received_sequence == sequence_number +assert received_length == len(received_payload) + +print("\nВсе проверки заголовка пройдены.") + +restored_message = bytes(received_payload).decode("utf-8") + + +print("\nСообщение после повреждения:") +print(restored_message) + +print("\nИсходное сообщение:") +print(message) + + +# ============================================================ +# Проверка результата эксперимента +# ============================================================ + +assert restored_message != message + +print("\nПовреждение данных произошло.") +print("Но заголовок и длина пакета остались правильными.") +print("Без CRC приёмник не смог определить ошибку.") \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab002_packet_structure.py b/Projects/SDR_Rover/tests/lab002_packet_structure.py new file mode 100644 index 0000000..2205b13 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab002_packet_structure.py @@ -0,0 +1,146 @@ +""" +Lab002. Формирование и разбор первого цифрового пакета. + +Пакет содержит: +- маркер начала; +- версию протокола; +- тип сообщения; +- порядковый номер; +- длину полезных данных; +- полезные данные. +""" + +import struct + + +# ============================================================ +# Константы протокола +# ============================================================ + +SYNC_WORD = 0xAA55 +PROTOCOL_VERSION = 1 + +MESSAGE_TYPE_TEXT = 1 + + +# ============================================================ +# Исходные данные +# ============================================================ + +message = "ПРИВЕТ SDR" +payload = message.encode("utf-8") + +sequence_number = 1 +message_type = MESSAGE_TYPE_TEXT +payload_length = len(payload) + + +# ============================================================ +# Формирование заголовка +# ============================================================ + +# Формат: +# > — порядок байтов от старшего к младшему, big-endian +# H — беззнаковое целое число размером 2 байта +# B — беззнаковое целое число размером 1 байт +# B — ещё одно число размером 1 байт +# H — порядковый номер размером 2 байта +# H — длина данных размером 2 байта +HEADER_FORMAT = ">HBBHH" + +header = struct.pack( + HEADER_FORMAT, + SYNC_WORD, + PROTOCOL_VERSION, + message_type, + sequence_number, + payload_length, +) + +packet = header + payload + + +# ============================================================ +# Вывод сформированного пакета +# ============================================================ + +print("Исходное сообщение:") +print(message) + +print("\nПолезная нагрузка PAYLOAD:") +print(payload) + +print("\nДлина PAYLOAD:") +print(payload_length, "байт") + +print("\nЗаголовок пакета:") +print(header) + +print("\nПолный пакет:") +print(packet) + +print("\nПакет в шестнадцатеричном виде:") +print(packet.hex(" ")) + +print("\nОбщая длина пакета:") +print(len(packet), "байт") + + +# ============================================================ +# Разбор пакета на стороне приёмника +# ============================================================ + +header_size = struct.calcsize(HEADER_FORMAT) + +received_header = packet[:header_size] +received_payload = packet[header_size:] + +( + received_sync, + received_version, + received_type, + received_sequence, + received_length, +) = struct.unpack(HEADER_FORMAT, received_header) + + +print("\n--- Разбор принятого пакета ---") + +print("SYNC:") +print(hex(received_sync)) + +print("\nВерсия протокола:") +print(received_version) + +print("\nТип сообщения:") +print(received_type) + +print("\nПорядковый номер:") +print(received_sequence) + +print("\nДлина из заголовка:") +print(received_length, "байт") + +print("\nФактически принято данных:") +print(len(received_payload), "байт") + + +# ============================================================ +# Проверки +# ============================================================ + +assert received_sync == SYNC_WORD +assert received_version == PROTOCOL_VERSION +assert received_type == MESSAGE_TYPE_TEXT +assert received_sequence == sequence_number +assert received_length == len(received_payload) + +restored_message = received_payload.decode("utf-8") + +assert restored_message == message + + +print("\nВосстановленное сообщение:") +print(restored_message) + +print("\nПроверка пройдена: пакет сформирован и разобран без ошибок.") \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab003_crc32.py b/Projects/SDR_Rover/tests/lab003_crc32.py new file mode 100644 index 0000000..3347044 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab003_crc32.py @@ -0,0 +1,198 @@ +""" +Lab003. Обнаружение повреждения пакета с помощью CRC-32. + +Цель: +1. Сформировать пакет с CRC-32. +2. Проверить исправный пакет. +3. Изменить один бит полезной нагрузки. +4. Убедиться, что CRC обнаруживает повреждение. +""" + +import struct +import zlib + + +# ============================================================ +# Константы протокола +# ============================================================ + +SYNC_WORD = 0xAA55 +PROTOCOL_VERSION = 1 +MESSAGE_TYPE_TEXT = 1 + +HEADER_FORMAT = ">HBBHH" +CRC_FORMAT = ">I" + +CRC_SIZE = struct.calcsize(CRC_FORMAT) + + +# ============================================================ +# Формирование исходного пакета +# ============================================================ + +message = "ПРИВЕТ SDR" +payload = message.encode("utf-8") + +sequence_number = 1 +payload_length = len(payload) + +header = struct.pack( + HEADER_FORMAT, + SYNC_WORD, + PROTOCOL_VERSION, + MESSAGE_TYPE_TEXT, + sequence_number, + payload_length, +) + +# CRC вычисляется по заголовку и полезной нагрузке. +packet_without_crc = header + payload + +crc_value = zlib.crc32(packet_without_crc) & 0xFFFFFFFF + +crc_bytes = struct.pack( + CRC_FORMAT, + crc_value, +) + +packet = packet_without_crc + crc_bytes + + +print("Исходное сообщение:") +print(message) + +print("\nCRC-32 исходного пакета:") +print(f"0x{crc_value:08X}") + +print("\nПолный пакет:") +print(packet.hex(" ")) + +print("\nДлина полного пакета:") +print(len(packet), "байт") + + +# ============================================================ +# Проверка исправного пакета +# ============================================================ + +received_data = packet[:-CRC_SIZE] +received_crc_bytes = packet[-CRC_SIZE:] + +(received_crc,) = struct.unpack( + CRC_FORMAT, + received_crc_bytes, +) + +calculated_crc = zlib.crc32(received_data) & 0xFFFFFFFF + + +print("\n--- Проверка исправного пакета ---") + +print(f"CRC из пакета: 0x{received_crc:08X}") +print(f"CRC вычисленный: 0x{calculated_crc:08X}") + +assert received_crc == calculated_crc + +print("Результат: пакет не повреждён.") + + +# ============================================================ +# Имитация повреждения одного бита +# ============================================================ + +corrupted_packet = bytearray(packet) + +header_size = struct.calcsize(HEADER_FORMAT) + +# Буква S находится по индексу 13 внутри PAYLOAD. +payload_byte_index = 13 +packet_byte_index = header_size + payload_byte_index + +original_byte = corrupted_packet[packet_byte_index] + +# Меняем младший бит: +# S = 0x53 +# R = 0x52 +corrupted_packet[packet_byte_index] ^= 0x01 + +corrupted_byte = corrupted_packet[packet_byte_index] + + +print("\n--- Повреждение одного бита ---") + +print( + f"До повреждения: " + f"0x{original_byte:02X} = {original_byte:08b}" +) + +print( + f"После повреждения: " + f"0x{corrupted_byte:02X} = {corrupted_byte:08b}" +) + + +# ============================================================ +# Проверка повреждённого пакета +# ============================================================ + +corrupted_data = bytes( + corrupted_packet[:-CRC_SIZE] +) + +corrupted_crc_bytes = bytes( + corrupted_packet[-CRC_SIZE:] +) + +(received_crc_after_corruption,) = struct.unpack( + CRC_FORMAT, + corrupted_crc_bytes, +) + +calculated_crc_after_corruption = ( + zlib.crc32(corrupted_data) & 0xFFFFFFFF +) + + +print("\n--- Проверка повреждённого пакета ---") + +print( + f"CRC из пакета: " + f"0x{received_crc_after_corruption:08X}" +) + +print( + f"CRC вычисленный: " + f"0x{calculated_crc_after_corruption:08X}" +) + + +if received_crc_after_corruption != calculated_crc_after_corruption: + print("\nРезультат: CRC обнаружил повреждение пакета.") +else: + print("\nРезультат: повреждение не обнаружено.") + + +# ============================================================ +# Показываем повреждённое сообщение +# ============================================================ + +corrupted_payload = corrupted_data[header_size:] + +corrupted_message = corrupted_payload.decode("utf-8") + +print("\nСообщение внутри повреждённого пакета:") +print(corrupted_message) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert corrupted_message == "ПРИВЕТ RDR" + +assert ( + received_crc_after_corruption + != calculated_crc_after_corruption +) + +print("\nПроверка пройдена: CRC-32 обнаружил изменение одного бита.") \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab004_protocol_module.py b/Projects/SDR_Rover/tests/lab004_protocol_module.py new file mode 100644 index 0000000..02cd93c --- /dev/null +++ b/Projects/SDR_Rover/tests/lab004_protocol_module.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 10 17:19:11 2026 + +@author: user +""" + +""" +Lab004. Проверка модуля protocol.packet. + +Проверяем: +1. Формирование пакета. +2. Разбор исправного пакета. +3. Восстановление текста. +4. Обнаружение повреждения CRC. +""" + +from protocol.packet import ( + CRCError, + HEADER_SIZE, + MESSAGE_TYPE_TEXT, + build_packet, + parse_packet, +) + + +# ============================================================ +# Формирование пакета +# ============================================================ + +message = "ПРИВЕТ SDR" +payload = message.encode("utf-8") + +sequence_number = 7 + +packet = build_packet( + payload=payload, + message_type=MESSAGE_TYPE_TEXT, + sequence_number=sequence_number, +) + + +print("Исходное сообщение:") +print(message) + +print("\nСформированный пакет:") +print(packet.hex(" ")) + +print("\nРазмер пакета:") +print(len(packet), "байт") + + +# ============================================================ +# Разбор исправного пакета +# ============================================================ + +parsed_packet = parse_packet(packet) + +restored_message = parsed_packet.payload.decode("utf-8") + + +print("\n--- Разобранный пакет ---") + +print("Версия:") +print(parsed_packet.version) + +print("\nТип сообщения:") +print(parsed_packet.message_type) + +print("\nПорядковый номер:") +print(parsed_packet.sequence_number) + +print("\nПолезная нагрузка:") +print(parsed_packet.payload) + +print("\nВосстановленное сообщение:") +print(restored_message) + + +assert parsed_packet.message_type == MESSAGE_TYPE_TEXT +assert parsed_packet.sequence_number == sequence_number +assert restored_message == message + +print("\nИсправный пакет успешно разобран.") + + +# ============================================================ +# Повреждение одного бита +# ============================================================ + +corrupted_packet = bytearray(packet) + +# В строке "ПРИВЕТ SDR" буква S находится +# по индексу 13 внутри полезной нагрузки. +payload_byte_index = 13 + +packet_byte_index = ( + HEADER_SIZE + payload_byte_index +) + +corrupted_packet[packet_byte_index] ^= 0x01 + + +print("\n--- Проверка повреждённого пакета ---") + + +try: + parse_packet(corrupted_packet) + +except CRCError as error: + print("Повреждение обнаружено.") + print(error) + +else: + raise AssertionError( + "Ошибка: повреждённый пакет был принят как исправный" + ) + + +print("\nВсе проверки Lab004 успешно выполнены.") \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab005_virtual_channel.py b/Projects/SDR_Rover/tests/lab005_virtual_channel.py new file mode 100644 index 0000000..545a257 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab005_virtual_channel.py @@ -0,0 +1,254 @@ +""" +Lab005. Передача пакетов через виртуальный радиоканал. + +Эксперимент: +1. Формируем 100 цифровых пакетов. +2. Передаём их через виртуальный канал. +3. Канал может: + - полностью потерять пакет; + - изменить отдельные биты. +4. Приёмник проверяет структуру пакета и CRC-32. +5. Выводится статистика доставки. +""" + +import random + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_TEXT, + PacketError, + build_packet, + parse_packet, +) + +from tools.virtual_channel import transmit + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +# Количество передаваемых пакетов. +PACKET_COUNT = 100 + +# Вероятность полной потери одного пакета. +# +# 0.10 означает 10 %. +DROP_PROBABILITY = 0.10 + +# Вероятность повреждения каждого отдельного бита. +# +# 0.002 означает: +# 0,2 % вероятности ошибки для каждого бита. +BIT_ERROR_PROBABILITY = 0.002 + +# Генератор случайных чисел. +# +# Число 2026 позволяет получать одинаковый результат +# при каждом запуске программы. +random_generator = random.Random(2026) + + +# ============================================================ +# Счётчики результатов +# ============================================================ + +sent_packets = 0 + +# Пакеты, полностью исчезнувшие в канале. +dropped_packets = 0 + +# Доставленные, но повреждённые пакеты. +rejected_packets = 0 + +# Пакеты, правильно принятые и разобранные. +received_ok = 0 + +# Общее количество изменённых битов. +total_flipped_bits = 0 + + +# ============================================================ +# Передача серии пакетов +# ============================================================ + +for sequence_number in range(PACKET_COUNT): + + # Создаём уникальное сообщение для каждого пакета. + message = f"ПАКЕТ {sequence_number}" + + # Текст превращается в байты UTF-8. + payload = message.encode("utf-8") + + # Формируем полный пакет с заголовком и CRC-32. + packet = build_packet( + payload=payload, + message_type=MESSAGE_TYPE_TEXT, + sequence_number=sequence_number, + ) + + sent_packets += 1 + + # Передаём пакет через виртуальный канал. + received_packet, flipped_bits = transmit( + packet=packet, + drop_probability=DROP_PROBABILITY, + bit_error_probability=BIT_ERROR_PROBABILITY, + random_generator=random_generator, + ) + + total_flipped_bits += flipped_bits + + # None означает, что пакет полностью потерян. + if received_packet is None: + dropped_packets += 1 + continue + + # Пытаемся разобрать принятый пакет. + try: + parsed_packet = parse_packet(received_packet) + + except (CRCError, PacketError): + # CRCError: + # контрольная сумма не совпала. + # + # PacketError: + # повреждены SYNC, версия, длина + # или другая часть структуры. + rejected_packets += 1 + continue + + # Восстанавливаем текст из полезной нагрузки. + restored_message = parsed_packet.payload.decode("utf-8") + + # Проверяем, что получен именно тот пакет, + # который был передан. + assert parsed_packet.sequence_number == sequence_number + assert restored_message == message + + received_ok += 1 + + +# ============================================================ +# Вывод настроек +# ============================================================ + +print("=== Lab005. Виртуальный канал ===") + +print("\nНастройки эксперимента:") + +print( + "Количество пакетов:", + PACKET_COUNT, +) + +print( + "Вероятность полной потери пакета:", + DROP_PROBABILITY, +) + +print( + "Вероятность ошибки отдельного бита:", + BIT_ERROR_PROBABILITY, +) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print("\nРезультаты:") + +print( + "Передано пакетов:", + sent_packets, +) + +print( + "Потеряно полностью:", + dropped_packets, +) + +print( + "Отклонено из-за повреждений:", + rejected_packets, +) + +print( + "Принято правильно:", + received_ok, +) + +print( + "Всего изменено битов:", + total_flipped_bits, +) + + +# ============================================================ +# Расчёт процентов +# ============================================================ + +delivery_rate = ( + received_ok + / sent_packets + * 100 +) + +drop_rate = ( + dropped_packets + / sent_packets + * 100 +) + +rejection_rate = ( + rejected_packets + / sent_packets + * 100 +) + +packet_failure_rate = ( + dropped_packets + + rejected_packets +) / sent_packets * 100 + + +print("\nСтатистика:") + +print( + f"Успешно доставлено: " + f"{delivery_rate:.1f} %" +) + +print( + f"Потеряно полностью: " + f"{drop_rate:.1f} %" +) + +print( + f"Отклонено приёмником: " + f"{rejection_rate:.1f} %" +) + +print( + f"Общая доля недоставленных пакетов: " + f"{packet_failure_rate:.1f} %" +) + + +# ============================================================ +# Контроль правильности +# ============================================================ + +accounted_packets = ( + received_ok + + dropped_packets + + rejected_packets +) + +assert accounted_packets == sent_packets + +print( + "\nПроверка пройдена: " + "все переданные пакеты учтены." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab006_stop_and_wait_arq.py b/Projects/SDR_Rover/tests/lab006_stop_and_wait_arq.py new file mode 100644 index 0000000..f62efaa --- /dev/null +++ b/Projects/SDR_Rover/tests/lab006_stop_and_wait_arq.py @@ -0,0 +1,325 @@ +""" +Lab006. Надёжная передача пакетов методом Stop-and-Wait ARQ. + +Передатчик: +1. Отправляет пакет. +2. Ожидает подтверждение ACK. +3. При отсутствии правильного ACK повторяет передачу. +4. После исчерпания попыток считает сообщение недоставленным. + +Приёмник: +1. Проверяет пакет. +2. Отбрасывает повреждённые пакеты. +3. Отправляет ACK для правильного пакета. +4. Не передаёт приложению повторные копии одного сообщения. +""" + +import random + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_TEXT, + PacketError, + build_packet, + parse_packet, +) + +from tools.virtual_channel import transmit + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +MESSAGE_COUNT = 100 + +# Максимальное количество передач одного сообщения. +MAX_ATTEMPTS = 5 + +# Параметры канала от передатчика к приёмнику. +DATA_DROP_PROBABILITY = 0.10 +DATA_BIT_ERROR_PROBABILITY = 0.002 + +# Параметры обратного канала для ACK. +ACK_DROP_PROBABILITY = 0.10 +ACK_BIT_ERROR_PROBABILITY = 0.002 + +# Фиксированное число делает эксперимент повторяемым. +random_generator = random.Random(2026) + + +# ============================================================ +# Счётчики +# ============================================================ + +delivered_messages = 0 +failed_messages = 0 + +data_transmissions = 0 +ack_transmissions = 0 + +duplicate_packets = 0 + +invalid_data_packets = 0 +invalid_ack_packets = 0 + +dropped_data_packets = 0 +dropped_ack_packets = 0 + +# Номера пакетов, уже переданных приложению приёмника. +received_sequences = set() + + +# ============================================================ +# Передача сообщений +# ============================================================ + +for sequence_number in range(MESSAGE_COUNT): + + message = f"ПАКЕТ {sequence_number}" + payload = message.encode("utf-8") + + data_packet = build_packet( + payload=payload, + message_type=MESSAGE_TYPE_TEXT, + sequence_number=sequence_number, + ) + + message_confirmed = False + + # Повторяем передачу до MAX_ATTEMPTS раз. + for attempt_number in range(1, MAX_ATTEMPTS + 1): + + data_transmissions += 1 + + received_data_packet, _ = transmit( + packet=data_packet, + drop_probability=DATA_DROP_PROBABILITY, + bit_error_probability=DATA_BIT_ERROR_PROBABILITY, + random_generator=random_generator, + ) + + # Пакет полностью потерялся. + if received_data_packet is None: + dropped_data_packets += 1 + continue + + # Приёмник проверяет пакет. + try: + parsed_data = parse_packet(received_data_packet) + + except (CRCError, PacketError): + invalid_data_packets += 1 + continue + + # Проверка типа сообщения. + if parsed_data.message_type != MESSAGE_TYPE_TEXT: + invalid_data_packets += 1 + continue + + # Проверяем, не получали ли мы этот пакет ранее. + if parsed_data.sequence_number in received_sequences: + duplicate_packets += 1 + + else: + restored_message = parsed_data.payload.decode("utf-8") + + assert restored_message == message + + received_sequences.add( + parsed_data.sequence_number + ) + + # Формируем подтверждение. + # + # ACK имеет тот же sequence_number, + # что и подтверждаемый пакет. + ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=parsed_data.sequence_number, + ) + + ack_transmissions += 1 + + received_ack_packet, _ = transmit( + packet=ack_packet, + drop_probability=ACK_DROP_PROBABILITY, + bit_error_probability=ACK_BIT_ERROR_PROBABILITY, + random_generator=random_generator, + ) + + # ACK полностью потерялся. + if received_ack_packet is None: + dropped_ack_packets += 1 + continue + + # Передатчик проверяет ACK. + try: + parsed_ack = parse_packet(received_ack_packet) + + except (CRCError, PacketError): + invalid_ack_packets += 1 + continue + + # ACK должен иметь правильный тип и номер. + if ( + parsed_ack.message_type != MESSAGE_TYPE_ACK + or parsed_ack.sequence_number != sequence_number + ): + invalid_ack_packets += 1 + continue + + # Подтверждение получено. + message_confirmed = True + break + + if message_confirmed: + delivered_messages += 1 + else: + failed_messages += 1 + + +# ============================================================ +# Расчёт статистики +# ============================================================ + +delivery_rate = ( + delivered_messages + / MESSAGE_COUNT + * 100 +) + +failure_rate = ( + failed_messages + / MESSAGE_COUNT + * 100 +) + +average_data_transmissions = ( + data_transmissions + / MESSAGE_COUNT +) + + +# ============================================================ +# Вывод результата +# ============================================================ + +print("=== Lab006. Stop-and-Wait ARQ ===") + +print("\nНастройки:") + +print("Количество сообщений:", MESSAGE_COUNT) +print("Максимум попыток:", MAX_ATTEMPTS) + +print( + "Вероятность потери DATA:", + DATA_DROP_PROBABILITY, +) + +print( + "Вероятность битовой ошибки DATA:", + DATA_BIT_ERROR_PROBABILITY, +) + +print( + "Вероятность потери ACK:", + ACK_DROP_PROBABILITY, +) + +print( + "Вероятность битовой ошибки ACK:", + ACK_BIT_ERROR_PROBABILITY, +) + + +print("\nОсновные результаты:") + +print( + "Успешно подтверждено сообщений:", + delivered_messages, +) + +print( + "Не подтверждено сообщений:", + failed_messages, +) + +print( + f"Доля успешной доставки: " + f"{delivery_rate:.1f} %" +) + +print( + f"Доля недоставленных сообщений: " + f"{failure_rate:.1f} %" +) + + +print("\nПередачи:") + +print( + "Всего передач DATA:", + data_transmissions, +) + +print( + "Всего передач ACK:", + ack_transmissions, +) + +print( + f"Среднее число передач DATA " + f"на одно сообщение: " + f"{average_data_transmissions:.2f}" +) + + +print("\nОшибки канала:") + +print( + "Полностью потеряно DATA:", + dropped_data_packets, +) + +print( + "Повреждено DATA:", + invalid_data_packets, +) + +print( + "Полностью потеряно ACK:", + dropped_ack_packets, +) + +print( + "Повреждено ACK:", + invalid_ack_packets, +) + +print( + "Получено повторных DATA-пакетов:", + duplicate_packets, +) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert ( + delivered_messages + + failed_messages + == MESSAGE_COUNT +) + +assert delivered_messages <= len(received_sequences) + +assert data_transmissions >= MESSAGE_COUNT + +print( + "\nПроверка пройдена: " + "все сообщения учтены." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab007_image_fragmentation.py b/Projects/SDR_Rover/tests/lab007_image_fragmentation.py new file mode 100644 index 0000000..e05c6a2 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab007_image_fragmentation.py @@ -0,0 +1,270 @@ +""" +Lab007. Разбиение JPEG на фрагменты и обратная сборка. + +Этапы: +1. Создание тестового JPEG. +2. Чтение файла как последовательности байтов. +3. Разбиение изображения на фрагменты. +4. Формирование цифрового пакета для каждого фрагмента. +5. Перемешивание порядка доставки пакетов. +6. Проверка CRC и разбор пакетов. +7. Обратная сборка изображения. +8. Сравнение контрольных сумм файлов. +""" + +from hashlib import sha256 +from pathlib import Path +import random + +from PIL import Image + +from protocol.image_fragments import ( + decode_image_fragment, + encode_image_fragment, + reassemble_image, + split_image_bytes, +) + +from protocol.packet import ( + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, + parse_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +IMAGE_WIDTH = 320 +IMAGE_HEIGHT = 240 + +IMAGE_ID = 20260713 + +FRAGMENT_DATA_SIZE = 512 + +random_generator = random.Random(2026) + + +# ============================================================ +# Пути к файлам +# ============================================================ + +source_directory = Path("data/raw") +received_directory = Path("data/received") + +source_directory.mkdir( + parents=True, + exist_ok=True, +) + +received_directory.mkdir( + parents=True, + exist_ok=True, +) + +source_path = ( + source_directory + / "lab007_source.jpg" +) + +received_path = ( + received_directory + / "lab007_reassembled.jpg" +) + + +# ============================================================ +# Создание тестового JPEG +# ============================================================ + +image = Image.new( + "RGB", + (IMAGE_WIDTH, IMAGE_HEIGHT), +) + +pixels = image.load() + +for y in range(IMAGE_HEIGHT): + for x in range(IMAGE_WIDTH): + + red = ( + x * 255 + // (IMAGE_WIDTH - 1) + ) + + green = ( + y * 255 + // (IMAGE_HEIGHT - 1) + ) + + blue = ( + (x + y) * 255 + // ( + IMAGE_WIDTH + + IMAGE_HEIGHT + - 2 + ) + ) + + pixels[x, y] = ( + red, + green, + blue, + ) + +image.save( + source_path, + format="JPEG", + quality=60, + optimize=True, +) + + +# ============================================================ +# Чтение изображения +# ============================================================ + +source_bytes = source_path.read_bytes() + +source_hash = sha256( + source_bytes +).hexdigest() + + +# ============================================================ +# Разбиение изображения на фрагменты +# ============================================================ + +fragments = split_image_bytes( + image_bytes=source_bytes, + image_id=IMAGE_ID, + fragment_data_size=FRAGMENT_DATA_SIZE, +) + + +# ============================================================ +# Формирование цифровых пакетов +# ============================================================ + +packets = [] + +for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + packet = build_packet( + payload=fragment_payload, + message_type=MESSAGE_TYPE_IMAGE_FRAGMENT, + sequence_number=fragment.fragment_index, + ) + + packets.append(packet) + + +# ============================================================ +# Имитация доставки в произвольном порядке +# ============================================================ + +random_generator.shuffle(packets) + +received_fragments = [] + +for packet in packets: + + parsed_packet = parse_packet(packet) + + assert ( + parsed_packet.message_type + == MESSAGE_TYPE_IMAGE_FRAGMENT + ) + + fragment = decode_image_fragment( + parsed_packet.payload + ) + + assert ( + parsed_packet.sequence_number + == fragment.fragment_index + ) + + received_fragments.append(fragment) + + +# ============================================================ +# Сборка изображения +# ============================================================ + +received_bytes = reassemble_image( + received_fragments +) + +received_path.write_bytes( + received_bytes +) + +received_hash = sha256( + received_bytes +).hexdigest() + + +# ============================================================ +# Проверки +# ============================================================ + +assert received_bytes == source_bytes +assert received_hash == source_hash + +with Image.open(received_path) as received_image: + received_image.verify() + + +# ============================================================ +# Результаты +# ============================================================ + +print("=== Lab007. Фрагментация изображения ===") + +print("\nИсходный файл:") +print(source_path) + +print("\nВосстановленный файл:") +print(received_path) + +print("\nРазмер изображения:") +print( + IMAGE_WIDTH, + "x", + IMAGE_HEIGHT, + "пикселей", +) + +print("\nРазмер JPEG:") +print( + len(source_bytes), + "байт", +) + +print("\nРазмер данных одного фрагмента:") +print( + FRAGMENT_DATA_SIZE, + "байт", +) + +print("\nКоличество фрагментов:") +print( + len(fragments) +) + +print("\nSHA-256 исходного файла:") +print(source_hash) + +print("\nSHA-256 восстановленного файла:") +print(received_hash) + +print( + "\nПроверка пройдена: " + "JPEG восстановлен байт в байт." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab008_image_transfer_arq.py b/Projects/SDR_Rover/tests/lab008_image_transfer_arq.py new file mode 100644 index 0000000..c6143b2 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab008_image_transfer_arq.py @@ -0,0 +1,428 @@ +""" +Lab008. Передача JPEG через виртуальный канал с ARQ. + +Этапы: +1. Чтение тестового JPEG из Lab007. +2. Разбиение файла на фрагменты. +3. Передача каждого фрагмента через виртуальный канал. +4. Проверка CRC на приёмнике. +5. Передача ACK по обратному каналу. +6. Повторная передача при потере или повреждении. +7. Сборка принятого JPEG. +8. Сравнение SHA-256 исходного и принятого файлов. +""" + +from hashlib import sha256 +from pathlib import Path +import random + +from PIL import Image + +from protocol.image_fragments import ( + ImageFragmentError, + decode_image_fragment, + encode_image_fragment, + reassemble_image, + split_image_bytes, +) + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + PacketError, + build_packet, + parse_packet, +) + +from tools.virtual_channel import transmit + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +IMAGE_ID = 20260713 +FRAGMENT_DATA_SIZE = 512 + +# Максимальное количество передач одного фрагмента. +MAX_ATTEMPTS = 12 + +# Прямой канал: передатчик → приёмник. +DATA_DROP_PROBABILITY = 0.10 +DATA_BIT_ERROR_PROBABILITY = 0.0002 + +# Обратный канал: приёмник → передатчик. +ACK_DROP_PROBABILITY = 0.10 +ACK_BIT_ERROR_PROBABILITY = 0.0002 + +# Фиксированное значение делает результат воспроизводимым. +random_generator = random.Random(2026) + + +# ============================================================ +# Пути к файлам +# ============================================================ + +source_path = Path( + "data/raw/lab007_source.jpg" +) + +received_path = Path( + "data/received/lab008_received.jpg" +) + +if not source_path.exists(): + raise FileNotFoundError( + "Не найден файл data/raw/lab007_source.jpg. " + "Сначала необходимо выполнить Lab007." + ) + +received_path.parent.mkdir( + parents=True, + exist_ok=True, +) + + +# ============================================================ +# Чтение и разбиение изображения +# ============================================================ + +source_bytes = source_path.read_bytes() + +source_hash = sha256( + source_bytes +).hexdigest() + +source_fragments = split_image_bytes( + image_bytes=source_bytes, + image_id=IMAGE_ID, + fragment_data_size=FRAGMENT_DATA_SIZE, +) + + +# ============================================================ +# Счётчики +# ============================================================ + +data_transmissions = 0 +ack_transmissions = 0 + +dropped_data_packets = 0 +invalid_data_packets = 0 + +dropped_ack_packets = 0 +invalid_ack_packets = 0 + +duplicate_data_packets = 0 + +flipped_data_bits = 0 +flipped_ack_bits = 0 + +confirmed_fragments = 0 +failed_fragment_indexes = [] + +# Принятые фрагменты хранятся по их номеру. +received_fragments = {} + +# Число попыток передачи каждого фрагмента. +attempts_per_fragment = {} + + +# ============================================================ +# Передача всех фрагментов +# ============================================================ + +for source_fragment in source_fragments: + + fragment_index = source_fragment.fragment_index + + fragment_payload = encode_image_fragment( + source_fragment + ) + + data_packet = build_packet( + payload=fragment_payload, + message_type=MESSAGE_TYPE_IMAGE_FRAGMENT, + sequence_number=fragment_index, + ) + + fragment_confirmed = False + + for attempt_number in range( + 1, + MAX_ATTEMPTS + 1, + ): + data_transmissions += 1 + + received_data_packet, changed_bits = transmit( + packet=data_packet, + drop_probability=DATA_DROP_PROBABILITY, + bit_error_probability=DATA_BIT_ERROR_PROBABILITY, + random_generator=random_generator, + ) + + flipped_data_bits += changed_bits + + # DATA полностью потерян. + if received_data_packet is None: + dropped_data_packets += 1 + continue + + # Проверка структуры и CRC. + try: + parsed_data_packet = parse_packet( + received_data_packet + ) + + except (CRCError, PacketError): + invalid_data_packets += 1 + continue + + if ( + parsed_data_packet.message_type + != MESSAGE_TYPE_IMAGE_FRAGMENT + ): + invalid_data_packets += 1 + continue + + # Разбор заголовка фрагмента изображения. + try: + received_fragment = decode_image_fragment( + parsed_data_packet.payload + ) + + except ImageFragmentError: + invalid_data_packets += 1 + continue + + # Проверяем согласованность номеров. + if ( + parsed_data_packet.sequence_number + != received_fragment.fragment_index + ): + invalid_data_packets += 1 + continue + + if received_fragment.image_id != IMAGE_ID: + invalid_data_packets += 1 + continue + + # Сохраняем новый фрагмент либо распознаём дубликат. + existing_fragment = received_fragments.get( + received_fragment.fragment_index + ) + + if existing_fragment is None: + received_fragments[ + received_fragment.fragment_index + ] = received_fragment + + else: + # DATA мог прийти повторно, если предыдущий ACK потерялся. + assert existing_fragment.data == received_fragment.data + duplicate_data_packets += 1 + + # ==================================================== + # Формирование ACK + # ==================================================== + + ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=received_fragment.fragment_index, + ) + + ack_transmissions += 1 + + received_ack_packet, changed_ack_bits = transmit( + packet=ack_packet, + drop_probability=ACK_DROP_PROBABILITY, + bit_error_probability=ACK_BIT_ERROR_PROBABILITY, + random_generator=random_generator, + ) + + flipped_ack_bits += changed_ack_bits + + # ACK полностью потерян. + if received_ack_packet is None: + dropped_ack_packets += 1 + continue + + # Передатчик проверяет ACK. + try: + parsed_ack = parse_packet( + received_ack_packet + ) + + except (CRCError, PacketError): + invalid_ack_packets += 1 + continue + + if ( + parsed_ack.message_type != MESSAGE_TYPE_ACK + or parsed_ack.sequence_number != fragment_index + or parsed_ack.payload != b"" + ): + invalid_ack_packets += 1 + continue + + # Фрагмент успешно подтверждён. + fragment_confirmed = True + confirmed_fragments += 1 + + attempts_per_fragment[ + fragment_index + ] = attempt_number + + break + + if not fragment_confirmed: + failed_fragment_indexes.append( + fragment_index + ) + + attempts_per_fragment[ + fragment_index + ] = MAX_ATTEMPTS + + +# ============================================================ +# Результаты передачи +# ============================================================ + +print("=== Lab008. Передача JPEG с ARQ ===") + +print("\nИсходный файл:") +print(source_path) + +print("\nРазмер JPEG:") +print(len(source_bytes), "байт") + +print("\nКоличество фрагментов:") +print(len(source_fragments)) + +print("\nПодтверждено фрагментов:") +print(confirmed_fragments) + +print("\nПолучено уникальных фрагментов:") +print(len(received_fragments)) + +print("\nНе подтверждены фрагменты:") +print(failed_fragment_indexes) + + +print("\nПередачи:") + +print( + "Всего передач DATA:", + data_transmissions, +) + +print( + "Всего передач ACK:", + ack_transmissions, +) + + +print("\nОшибки прямого канала:") + +print( + "Полностью потеряно DATA:", + dropped_data_packets, +) + +print( + "Повреждено DATA:", + invalid_data_packets, +) + +print( + "Изменено битов DATA:", + flipped_data_bits, +) + + +print("\nОшибки обратного канала:") + +print( + "Полностью потеряно ACK:", + dropped_ack_packets, +) + +print( + "Повреждено ACK:", + invalid_ack_packets, +) + +print( + "Изменено битов ACK:", + flipped_ack_bits, +) + +print( + "Получено повторных DATA:", + duplicate_data_packets, +) + + +print("\nКоличество попыток по фрагментам:") + +for fragment_index in sorted( + attempts_per_fragment +): + print( + f"Фрагмент {fragment_index}: " + f"{attempts_per_fragment[fragment_index]}" + ) + + +# ============================================================ +# Сборка принятого изображения +# ============================================================ + +assert not failed_fragment_indexes + +assert ( + len(received_fragments) + == len(source_fragments) +) + +received_bytes = reassemble_image( + received_fragments.values() +) + +received_path.write_bytes( + received_bytes +) + +received_hash = sha256( + received_bytes +).hexdigest() + + +# ============================================================ +# Финальные проверки +# ============================================================ + +assert received_bytes == source_bytes +assert received_hash == source_hash + +with Image.open(received_path) as image: + image.verify() + + +print("\nSHA-256 исходного JPEG:") +print(source_hash) + +print("\nSHA-256 принятого JPEG:") +print(received_hash) + +print("\nПринятый файл:") +print(received_path) + +print( + "\nПроверка пройдена: " + "все фрагменты подтверждены, " + "JPEG восстановлен байт в байт." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab009_real_photo_estimate.py b/Projects/SDR_Rover/tests/lab009_real_photo_estimate.py new file mode 100644 index 0000000..96359a2 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab009_real_photo_estimate.py @@ -0,0 +1,450 @@ +""" +Lab009. Расчёт параметров передачи реальной фотографии. + +Программа: +1. Читает реальный JPEG-файл. +2. Определяет его разрешение и размер. +3. Разбивает файл на фрагменты. +4. Формирует реальные пакеты протокола. +5. Учитывает ACK для каждого фрагмента. +6. Рассчитывает время передачи при разных скоростях канала. + +Расчёт времени показывает чистое эфирное время без учёта: +- пауз между пакетами; +- обработки на передатчике и приёмнике; +- ожидания ACK; +- повторных передач; +- преамбулы и служебных символов физического уровня. +""" + +from pathlib import Path + +from PIL import Image + +from protocol.image_fragments import ( + FRAGMENT_HEADER_SIZE, + encode_image_fragment, + split_image_bytes, +) + +from protocol.packet import ( + CRC_SIZE, + HEADER_SIZE, + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +SOURCE_PATH = Path( + "data/raw/lab009_source.jpg" +) + +IMAGE_ID = 20260713 + +# Сколько байтов JPEG помещается в один фрагмент. +FRAGMENT_DATA_SIZE = 512 + +# Скорости полезного цифрового канала в кбит/с. +BITRATES_KBPS = [ + 1, + 5, + 10, + 20, + 50, + 100, + 200, +] + + +# ============================================================ +# Вспомогательные функции +# ============================================================ + +def format_bytes(byte_count: int) -> str: + """ + Представить размер одновременно в байтах и килобайтах. + """ + + kibibytes = byte_count / 1024 + + return ( + f"{byte_count} байт " + f"({kibibytes:.2f} КиБ)" + ) + + +def format_duration(seconds: float) -> str: + """ + Представить длительность в удобной форме. + """ + + if seconds < 1: + return f"{seconds * 1000:.1f} мс" + + if seconds < 60: + return f"{seconds:.2f} с" + + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + + return ( + f"{minutes} мин " + f"{remaining_seconds:.1f} с" + ) + + +# ============================================================ +# Проверка исходного файла +# ============================================================ + +if not SOURCE_PATH.exists(): + raise FileNotFoundError( + f"Не найден файл: {SOURCE_PATH}" + ) + +source_bytes = SOURCE_PATH.read_bytes() + +if not source_bytes: + raise ValueError( + "Исходный JPEG-файл пуст" + ) + + +# ============================================================ +# Чтение параметров изображения +# ============================================================ + +with Image.open(SOURCE_PATH) as image: + + image_format = image.format + image_mode = image.mode + image_width, image_height = image.size + + # Проверяем, что файл действительно читается как изображение. + image.verify() + + +# ============================================================ +# Разбиение JPEG на фрагменты +# ============================================================ + +fragments = split_image_bytes( + image_bytes=source_bytes, + image_id=IMAGE_ID, + fragment_data_size=FRAGMENT_DATA_SIZE, +) + + +# ============================================================ +# Формирование DATA-пакетов +# ============================================================ + +data_packets = [] + +for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + packet = build_packet( + payload=fragment_payload, + message_type=MESSAGE_TYPE_IMAGE_FRAGMENT, + sequence_number=fragment.fragment_index, + ) + + data_packets.append(packet) + + +# ============================================================ +# Формирование типового ACK-пакета +# ============================================================ + +ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=0, +) + +ack_packet_size = len(ack_packet) + + +# ============================================================ +# Расчёт объёмов +# ============================================================ + +source_size = len(source_bytes) + +fragment_count = len(fragments) + +# Полный размер всех DATA-пакетов. +total_data_packet_bytes = sum( + len(packet) + for packet in data_packets +) + +# Для каждого DATA-пакета ожидается один ACK. +total_ack_bytes = ( + fragment_count + * ack_packet_size +) + +# Идеальный радиообмен: +# каждый DATA и каждый ACK переданы ровно один раз. +total_radio_bytes = ( + total_data_packet_bytes + + total_ack_bytes +) + +total_radio_bits = ( + total_radio_bytes + * 8 +) + + +# ============================================================ +# Детализация служебных расходов +# ============================================================ + +total_fragment_header_bytes = ( + fragment_count + * FRAGMENT_HEADER_SIZE +) + +data_protocol_overhead = ( + fragment_count + * (HEADER_SIZE + CRC_SIZE) +) + +total_overhead_bytes = ( + total_radio_bytes + - source_size +) + +payload_efficiency = ( + source_size + / total_radio_bytes + * 100 +) + + +# ============================================================ +# Вывод параметров фотографии +# ============================================================ + +print("=== Lab009. Расчёт передачи реальной фотографии ===") + +print("\nИсходный файл:") +print(SOURCE_PATH) + +print("\nФормат изображения:") +print(image_format) + +print("\nЦветовой режим:") +print(image_mode) + +print("\nРазрешение:") +print( + image_width, + "x", + image_height, + "пикселей", +) + +print("\nРазмер исходного JPEG:") +print( + format_bytes(source_size) +) + + +# ============================================================ +# Вывод структуры передачи +# ============================================================ + +print("\n--- Фрагментация ---") + +print( + "Размер данных одного полного фрагмента:", + FRAGMENT_DATA_SIZE, + "байт", +) + +print( + "Количество фрагментов:", + fragment_count, +) + +print( + "Размер заголовка фрагмента:", + FRAGMENT_HEADER_SIZE, + "байт", +) + +print( + "Размер заголовка основного пакета:", + HEADER_SIZE, + "байт", +) + +print( + "Размер CRC-32:", + CRC_SIZE, + "байт", +) + +print( + "Размер ACK-пакета:", + ack_packet_size, + "байт", +) + + +# ============================================================ +# Вывод полного объёма +# ============================================================ + +print("\n--- Объём передачи без повторов ---") + +print("Исходные данные JPEG:") +print( + format_bytes(source_size) +) + +print("\nЗаголовки фрагментов:") +print( + format_bytes(total_fragment_header_bytes) +) + +print("\nСлужебные поля и CRC DATA-пакетов:") +print( + format_bytes(data_protocol_overhead) +) + +print("\nВсе DATA-пакеты:") +print( + format_bytes(total_data_packet_bytes) +) + +print("\nВсе ACK-пакеты:") +print( + format_bytes(total_ack_bytes) +) + +print("\nПолный радиообмен DATA + ACK:") +print( + format_bytes(total_radio_bytes) +) + +print("\nОбщие служебные расходы:") +print( + format_bytes(total_overhead_bytes) +) + +print( + "\nПолезная эффективность протокола:" +) + +print( + f"{payload_efficiency:.2f} %" +) + + +# ============================================================ +# Расчёт времени передачи +# ============================================================ + +print("\n--- Чистое эфирное время ---") + +print( + "Скорость DATA без ACK DATA + ACK" +) + +for bitrate_kbps in BITRATES_KBPS: + + bitrate_bits_per_second = ( + bitrate_kbps + * 1000 + ) + + data_time_seconds = ( + total_data_packet_bytes + * 8 + / bitrate_bits_per_second + ) + + full_time_seconds = ( + total_radio_bits + / bitrate_bits_per_second + ) + + print( + f"{bitrate_kbps:>4} кбит/с" + f" " + f"{format_duration(data_time_seconds):>12}" + f" " + f"{format_duration(full_time_seconds):>12}" + ) + + +# ============================================================ +# Сценарий с повторами +# ============================================================ + +# Коэффициент 2 означает, что суммарно передано +# примерно в два раза больше байтов из-за ошибок, +# потерь и повторных передач. +RETRY_TRAFFIC_MULTIPLIER = 2.0 + +print( + "\n--- Оценка при удвоении трафика из-за повторов ---" +) + +for bitrate_kbps in BITRATES_KBPS: + + bitrate_bits_per_second = ( + bitrate_kbps + * 1000 + ) + + estimated_seconds = ( + total_radio_bits + * RETRY_TRAFFIC_MULTIPLIER + / bitrate_bits_per_second + ) + + print( + f"{bitrate_kbps:>4} кбит/с" + f" " + f"{format_duration(estimated_seconds)}" + ) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert fragment_count > 0 + +assert ( + total_data_packet_bytes + > source_size +) + +assert ( + total_radio_bytes + > total_data_packet_bytes +) + +assert ( + 0 < payload_efficiency < 100 +) + +print( + "\nПроверка пройдена: " + "параметры передачи фотографии рассчитаны." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab010_image_optimization.py b/Projects/SDR_Rover/tests/lab010_image_optimization.py new file mode 100644 index 0000000..ab3d91f --- /dev/null +++ b/Projects/SDR_Rover/tests/lab010_image_optimization.py @@ -0,0 +1,785 @@ +""" +Lab010. Оптимизация изображения для слабого радиоканала. + +Программа формирует несколько вариантов одного кадра: + +1. Исходный JPEG. +2. Цветной кадр до 640x480. +3. Серый кадр до 640x480. +4. Серый кадр до 320x240, JPEG quality 30. +5. Серый кадр до 320x240, JPEG quality 15. +6. Карта контуров 320x240. +7. Серый кадр с наложенными контурами. + +Для каждого варианта рассчитываются: + +- размер JPEG; +- число фрагментов; +- полный объём DATA + ACK; +- эффективность протокола; +- время передачи на нескольких скоростях; +- время при удвоении трафика из-за повторов. +""" + +from csv import DictWriter +from hashlib import sha256 +from math import ceil +from pathlib import Path + +from PIL import ( + Image, + ImageDraw, + ImageFilter, + ImageOps, +) + +from protocol.image_fragments import ( + encode_image_fragment, + split_image_bytes, +) + +from protocol.packet import ( + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +SOURCE_PATH = Path( + "data/raw/lab009_source.jpg" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab010" +) + +FRAGMENT_DATA_SIZE = 512 + +IMAGE_ID_BASE = 2026071300 + +BITRATES_KBPS = [ + 5, + 10, + 20, + 50, + 100, +] + + +# ============================================================ +# Вспомогательные функции +# ============================================================ + +def resize_inside( + image: Image.Image, + maximum_size: tuple[int, int], +) -> Image.Image: + """ + Уменьшить изображение с сохранением пропорций. + + Изображение не растягивается и не искажается. + """ + + resized_image = image.copy() + + resized_image.thumbnail( + maximum_size, + Image.Resampling.LANCZOS, + ) + + return resized_image + + +def save_jpeg( + image: Image.Image, + output_path: Path, + quality: int, +) -> None: + """ + Сохранить изображение в JPEG. + """ + + output_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + image.save( + output_path, + format="JPEG", + quality=quality, + optimize=True, + ) + + +def format_bytes(byte_count: int) -> str: + """ + Представить размер в КиБ. + """ + + return f"{byte_count / 1024:.2f} КиБ" + + +def format_duration(seconds: float) -> str: + """ + Представить время в удобной форме. + """ + + if seconds < 1: + return f"{seconds * 1000:.0f} мс" + + if seconds < 60: + return f"{seconds:.2f} с" + + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + + return ( + f"{minutes} мин " + f"{remaining_seconds:.1f} с" + ) + + +def estimate_transfer( + file_path: Path, + image_id: int, +) -> dict: + """ + Рассчитать параметры передачи одного JPEG. + """ + + image_bytes = file_path.read_bytes() + + fragments = split_image_bytes( + image_bytes=image_bytes, + image_id=image_id, + fragment_data_size=FRAGMENT_DATA_SIZE, + ) + + total_data_packet_bytes = 0 + + for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + packet = build_packet( + payload=fragment_payload, + message_type=MESSAGE_TYPE_IMAGE_FRAGMENT, + sequence_number=fragment.fragment_index, + ) + + total_data_packet_bytes += len(packet) + + ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=0, + ) + + total_ack_bytes = ( + len(fragments) + * len(ack_packet) + ) + + total_radio_bytes = ( + total_data_packet_bytes + + total_ack_bytes + ) + + efficiency_percent = ( + len(image_bytes) + / total_radio_bytes + * 100 + ) + + transfer_times = {} + + for bitrate_kbps in BITRATES_KBPS: + + bitrate_bits_per_second = ( + bitrate_kbps * 1000 + ) + + transfer_times[bitrate_kbps] = ( + total_radio_bytes + * 8 + / bitrate_bits_per_second + ) + + return { + "file_size_bytes": len(image_bytes), + "fragment_count": len(fragments), + "data_packet_bytes": total_data_packet_bytes, + "ack_bytes": total_ack_bytes, + "total_radio_bytes": total_radio_bytes, + "efficiency_percent": efficiency_percent, + "transfer_times": transfer_times, + "sha256": sha256(image_bytes).hexdigest(), + } + + +# ============================================================ +# Проверка исходной фотографии +# ============================================================ + +if not SOURCE_PATH.exists(): + raise FileNotFoundError( + f"Не найден исходный файл: {SOURCE_PATH}" + ) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + + +# ============================================================ +# Чтение исходного изображения +# ============================================================ + +with Image.open(SOURCE_PATH) as source_image: + + source_image.load() + + source_rgb = source_image.convert("RGB") + + original_width, original_height = ( + source_rgb.size + ) + + +# ============================================================ +# Формирование вариантов изображения +# ============================================================ + +variant_paths = [] + +# ------------------------------------------------------------ +# Вариант 0. Исходный JPEG без изменения +# ------------------------------------------------------------ + +variant_paths.append( + ( + "00_original", + SOURCE_PATH, + ) +) + + +# ------------------------------------------------------------ +# Вариант 1. Цветной кадр до 640x480, quality 40 +# ------------------------------------------------------------ + +color_640 = resize_inside( + source_rgb, + (640, 480), +) + +color_640_path = ( + OUTPUT_DIRECTORY + / "01_color_640_q40.jpg" +) + +save_jpeg( + color_640, + color_640_path, + quality=40, +) + +variant_paths.append( + ( + "01_color_640_q40", + color_640_path, + ) +) + + +# ------------------------------------------------------------ +# Вариант 2. Серый кадр до 640x480, quality 40 +# ------------------------------------------------------------ + +gray_640 = ImageOps.grayscale( + color_640 +) + +gray_640_path = ( + OUTPUT_DIRECTORY + / "02_gray_640_q40.jpg" +) + +save_jpeg( + gray_640, + gray_640_path, + quality=40, +) + +variant_paths.append( + ( + "02_gray_640_q40", + gray_640_path, + ) +) + + +# ------------------------------------------------------------ +# Вариант 3. Серый кадр до 320x240, quality 30 +# ------------------------------------------------------------ + +color_320 = resize_inside( + source_rgb, + (320, 240), +) + +gray_320 = ImageOps.grayscale( + color_320 +) + +gray_320_q30_path = ( + OUTPUT_DIRECTORY + / "03_gray_320_q30.jpg" +) + +save_jpeg( + gray_320, + gray_320_q30_path, + quality=30, +) + +variant_paths.append( + ( + "03_gray_320_q30", + gray_320_q30_path, + ) +) + + +# ------------------------------------------------------------ +# Вариант 4. Серый кадр до 320x240, quality 15 +# ------------------------------------------------------------ + +gray_320_q15_path = ( + OUTPUT_DIRECTORY + / "04_gray_320_q15.jpg" +) + +save_jpeg( + gray_320, + gray_320_q15_path, + quality=15, +) + +variant_paths.append( + ( + "04_gray_320_q15", + gray_320_q15_path, + ) +) + + +# ------------------------------------------------------------ +# Вариант 5. Только контуры +# ------------------------------------------------------------ + +edge_map = gray_320.filter( + ImageFilter.FIND_EDGES +) + +edge_map = ImageOps.autocontrast( + edge_map +) + +# Инвертируем: белый фон, тёмные контуры. +edge_map = ImageOps.invert( + edge_map +) + +edges_path = ( + OUTPUT_DIRECTORY + / "05_edges_320_q40.jpg" +) + +save_jpeg( + edge_map, + edges_path, + quality=40, +) + +variant_paths.append( + ( + "05_edges_320_q40", + edges_path, + ) +) + + +# ------------------------------------------------------------ +# Вариант 6. Серый кадр с усиленными контурами +# ------------------------------------------------------------ + +gray_edges_overlay = Image.blend( + gray_320, + edge_map, + alpha=0.25, +) + +gray_edges_path = ( + OUTPUT_DIRECTORY + / "06_gray_edges_320_q30.jpg" +) + +save_jpeg( + gray_edges_overlay, + gray_edges_path, + quality=30, +) + +variant_paths.append( + ( + "06_gray_edges_320_q30", + gray_edges_path, + ) +) + + +# ============================================================ +# Расчёт параметров всех вариантов +# ============================================================ + +results = [] + +for variant_number, ( + variant_name, + variant_path, +) in enumerate(variant_paths): + + with Image.open(variant_path) as variant_image: + + width, height = variant_image.size + mode = variant_image.mode + + transfer_result = estimate_transfer( + file_path=variant_path, + image_id=IMAGE_ID_BASE + variant_number, + ) + + result = { + "name": variant_name, + "path": str(variant_path), + "width": width, + "height": height, + "mode": mode, + **transfer_result, + } + + results.append(result) + + +# ============================================================ +# Создание сравнительной картинки +# ============================================================ + +CONTACT_SHEET_COLUMNS = 3 +CONTACT_SHEET_CELL_WIDTH = 360 +CONTACT_SHEET_CELL_HEIGHT = 290 + +contact_sheet_rows = ceil( + len(results) + / CONTACT_SHEET_COLUMNS +) + +contact_sheet = Image.new( + "RGB", + ( + CONTACT_SHEET_COLUMNS + * CONTACT_SHEET_CELL_WIDTH, + contact_sheet_rows + * CONTACT_SHEET_CELL_HEIGHT, + ), + "white", +) + +draw = ImageDraw.Draw( + contact_sheet +) + +for result_index, result in enumerate(results): + + column = ( + result_index + % CONTACT_SHEET_COLUMNS + ) + + row = ( + result_index + // CONTACT_SHEET_COLUMNS + ) + + cell_x = ( + column + * CONTACT_SHEET_CELL_WIDTH + ) + + cell_y = ( + row + * CONTACT_SHEET_CELL_HEIGHT + ) + + with Image.open(result["path"]) as variant_image: + + preview = variant_image.convert("RGB") + + preview.thumbnail( + (330, 220), + Image.Resampling.LANCZOS, + ) + + paste_x = ( + cell_x + + ( + CONTACT_SHEET_CELL_WIDTH + - preview.width + ) + // 2 + ) + + paste_y = ( + cell_y + 35 + ) + + contact_sheet.paste( + preview, + (paste_x, paste_y), + ) + + label = ( + f"{result['name']}\n" + f"{format_bytes(result['file_size_bytes'])}, " + f"{result['fragment_count']} fragments" + ) + + draw.text( + (cell_x + 10, cell_y + 8), + label, + fill="black", + ) + +contact_sheet_path = ( + OUTPUT_DIRECTORY + / "lab010_comparison.jpg" +) + +contact_sheet.save( + contact_sheet_path, + format="JPEG", + quality=90, +) + + +# ============================================================ +# Сохранение таблицы CSV +# ============================================================ + +csv_path = ( + OUTPUT_DIRECTORY + / "lab010_results.csv" +) + +csv_fieldnames = [ + "name", + "path", + "width", + "height", + "mode", + "file_size_bytes", + "fragment_count", + "total_radio_bytes", + "efficiency_percent", + "time_5_kbps", + "time_10_kbps", + "time_20_kbps", + "time_50_kbps", + "time_100_kbps", + "time_20_kbps_with_retries_x2", +] + +with csv_path.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + writer = DictWriter( + csv_file, + fieldnames=csv_fieldnames, + ) + + writer.writeheader() + + for result in results: + + writer.writerow( + { + "name": result["name"], + "path": result["path"], + "width": result["width"], + "height": result["height"], + "mode": result["mode"], + "file_size_bytes": ( + result["file_size_bytes"] + ), + "fragment_count": ( + result["fragment_count"] + ), + "total_radio_bytes": ( + result["total_radio_bytes"] + ), + "efficiency_percent": ( + f"{result['efficiency_percent']:.2f}" + ), + "time_5_kbps": ( + result["transfer_times"][5] + ), + "time_10_kbps": ( + result["transfer_times"][10] + ), + "time_20_kbps": ( + result["transfer_times"][20] + ), + "time_50_kbps": ( + result["transfer_times"][50] + ), + "time_100_kbps": ( + result["transfer_times"][100] + ), + "time_20_kbps_with_retries_x2": ( + result["transfer_times"][20] + * 2 + ), + } + ) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print( + "=== Lab010. Оптимизация изображения ===" +) + +print("\nИсходное разрешение:") + +print( + original_width, + "x", + original_height, +) + + +print("\nСравнение вариантов:") + +print( + f"{'Вариант':<28}" + f"{'Размер':>12}" + f"{'Фрагм.':>9}" + f"{'10 кбит/с':>13}" + f"{'20 кбит/с':>13}" + f"{'50 кбит/с':>13}" + f"{'20 кбит/с x2':>16}" +) + +print("-" * 104) + +for result in results: + + print( + f"{result['name']:<28}" + f"{format_bytes(result['file_size_bytes']):>12}" + f"{result['fragment_count']:>9}" + f"{format_duration(result['transfer_times'][10]):>13}" + f"{format_duration(result['transfer_times'][20]):>13}" + f"{format_duration(result['transfer_times'][50]):>13}" + f"{format_duration(result['transfer_times'][20] * 2):>16}" + ) + + +# ============================================================ +# Итоговые кандидаты +# ============================================================ + +smallest_result = min( + results, + key=lambda item: item["file_size_bytes"], +) + +operator_candidate = next( + result + for result in results + if result["name"] == "03_gray_320_q30" +) + + +print("\nСамый маленький файл:") + +print( + smallest_result["name"], + "-", + format_bytes( + smallest_result["file_size_bytes"] + ), +) + + +print("\nБазовый кандидат для оператора:") + +print( + operator_candidate["name"], + "-", + format_bytes( + operator_candidate["file_size_bytes"] + ), + "-", + format_duration( + operator_candidate[ + "transfer_times" + ][20] + ), + "при 20 кбит/с без повторов", +) + + +print("\nСравнительная картинка:") + +print(contact_sheet_path) + + +print("\nТаблица результатов CSV:") + +print(csv_path) + + +# ============================================================ +# Проверки +# ============================================================ + +assert len(results) == 7 + +assert contact_sheet_path.exists() + +assert csv_path.exists() + +assert all( + result["fragment_count"] > 0 + for result in results +) + +print( + "\nПроверка пройдена: " + "варианты кадра созданы и рассчитаны." +) diff --git a/Projects/SDR_Rover/tests/lab011_scene_modes.py b/Projects/SDR_Rover/tests/lab011_scene_modes.py new file mode 100644 index 0000000..fbe2729 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab011_scene_modes.py @@ -0,0 +1,525 @@ +""" +Lab011. Проверка облегчённых режимов изображения +на сцене, похожей на вид с камеры ровера. + +Создаются варианты: +1. Grayscale 320x240, JPEG quality 30. +2. Grayscale 320x240, JPEG quality 15. +3. Grayscale 320x240 с наложением контуров, quality 30. + +Для каждого варианта рассчитываются: +- размер JPEG; +- количество радиопакетов; +- время передачи при разных скоростях. +""" + +from math import ceil +from pathlib import Path + +from PIL import ( + Image, + ImageChops, + ImageDraw, + ImageFilter, + ImageOps, +) + +from protocol.image_fragments import ( + encode_image_fragment, + split_image_bytes, +) + +from protocol.packet import ( + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +SOURCE_PATH = Path( + "data/raw/lab011_scene.jpg" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab011" +) + +FRAME_SIZE = (320, 240) + +FRAGMENT_DATA_SIZE = 512 + +IMAGE_ID_BASE = 2026071400 + +BITRATES_KBPS = [ + 5, + 10, + 20, + 50, +] + + +# ============================================================ +# Вспомогательные функции +# ============================================================ + +def prepare_frame( + image: Image.Image, +) -> Image.Image: + """ + Привести изображение к фиксированному кадру 320x240. + + Пропорции сохраняются. + Недостающие области заполняются чёрным. + """ + + image = ImageOps.exif_transpose(image) + image = image.convert("RGB") + + return ImageOps.pad( + image, + FRAME_SIZE, + method=Image.Resampling.LANCZOS, + color="black", + centering=(0.5, 0.5), + ) + + +def save_jpeg( + image: Image.Image, + path: Path, + quality: int, +) -> None: + """ + Сохранить изображение в JPEG. + """ + + path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + image.save( + path, + format="JPEG", + quality=quality, + optimize=True, + ) + + +def format_size( + byte_count: int, +) -> str: + """ + Представить размер в КиБ. + """ + + return f"{byte_count / 1024:.2f} КиБ" + + +def format_duration( + seconds: float, +) -> str: + """ + Представить длительность передачи. + """ + + if seconds < 1: + return f"{seconds * 1000:.0f} мс" + + if seconds < 60: + return f"{seconds:.2f} с" + + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + + return ( + f"{minutes} мин " + f"{remaining_seconds:.1f} с" + ) + + +def estimate_transfer( + file_path: Path, + image_id: int, +) -> dict: + """ + Рассчитать полный объём DATA + ACK. + """ + + image_bytes = file_path.read_bytes() + + fragments = split_image_bytes( + image_bytes=image_bytes, + image_id=image_id, + fragment_data_size=FRAGMENT_DATA_SIZE, + ) + + data_packet_bytes = 0 + + for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + packet = build_packet( + payload=fragment_payload, + message_type=MESSAGE_TYPE_IMAGE_FRAGMENT, + sequence_number=fragment.fragment_index, + ) + + data_packet_bytes += len(packet) + + ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=0, + ) + + ack_bytes = ( + len(fragments) + * len(ack_packet) + ) + + total_radio_bytes = ( + data_packet_bytes + + ack_bytes + ) + + times = {} + + for bitrate_kbps in BITRATES_KBPS: + + times[bitrate_kbps] = ( + total_radio_bytes + * 8 + / (bitrate_kbps * 1000) + ) + + return { + "file_size": len(image_bytes), + "fragment_count": len(fragments), + "total_radio_bytes": total_radio_bytes, + "times": times, + } + + +# ============================================================ +# Проверка исходного файла +# ============================================================ + +if not SOURCE_PATH.exists(): + raise FileNotFoundError( + f"Не найден файл: {SOURCE_PATH}" + ) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + + +# ============================================================ +# Подготовка базового кадра +# ============================================================ + +with Image.open(SOURCE_PATH) as source_image: + + original_size = source_image.size + + color_frame = prepare_frame( + source_image + ) + +gray_frame = ImageOps.grayscale( + color_frame +) + + +# ============================================================ +# Вариант 1. Grayscale, quality 30 +# ============================================================ + +gray_q30_path = ( + OUTPUT_DIRECTORY + / "01_gray_320_q30.jpg" +) + +save_jpeg( + gray_frame, + gray_q30_path, + quality=30, +) + + +# ============================================================ +# Вариант 2. Grayscale, quality 15 +# ============================================================ + +gray_q15_path = ( + OUTPUT_DIRECTORY + / "02_gray_320_q15.jpg" +) + +save_jpeg( + gray_frame, + gray_q15_path, + quality=15, +) + + +# ============================================================ +# Вариант 3. Grayscale + контуры +# ============================================================ + +edge_map = gray_frame.filter( + ImageFilter.FIND_EDGES +) + +edge_map = ImageOps.autocontrast( + edge_map +) + +# Оставляем преимущественно сильные контуры. +binary_edges = edge_map.point( + lambda value: 255 if value >= 45 else 0 +) + +# После инверсии контуры становятся чёрными, +# а фон — белым. +dark_edges = ImageOps.invert( + binary_edges +) + +# Сохраняем исходный серый фон и добавляем +# поверх него тёмные линии контуров. +gray_with_edges = ImageChops.darker( + gray_frame, + dark_edges +) + +gray_edges_path = ( + OUTPUT_DIRECTORY + / "03_gray_edges_320_q30.jpg" +) + +save_jpeg( + gray_with_edges, + gray_edges_path, + quality=30, +) + + +# ============================================================ +# Список вариантов +# ============================================================ + +variants = [ + ( + "gray_320_q30", + gray_q30_path, + ), + ( + "gray_320_q15", + gray_q15_path, + ), + ( + "gray_edges_320_q30", + gray_edges_path, + ), +] + + +# ============================================================ +# Расчёт параметров +# ============================================================ + +results = [] + +for variant_index, ( + variant_name, + variant_path, +) in enumerate(variants): + + result = estimate_transfer( + file_path=variant_path, + image_id=IMAGE_ID_BASE + variant_index, + ) + + results.append( + { + "name": variant_name, + "path": variant_path, + **result, + } + ) + + +# ============================================================ +# Создание сравнительной картинки +# ============================================================ + +preview_variants = [ + ( + "SOURCE PREVIEW", + color_frame, + None, + ) +] + +for result in results: + + with Image.open(result["path"]) as image: + + preview_variants.append( + ( + result["name"], + image.convert("RGB").copy(), + result, + ) + ) + + +columns = 2 +cell_width = 440 +cell_height = 340 + +rows = ceil( + len(preview_variants) + / columns +) + +comparison_image = Image.new( + "RGB", + ( + columns * cell_width, + rows * cell_height, + ), + "white", +) + +draw = ImageDraw.Draw( + comparison_image +) + +for index, ( + label, + preview, + result, +) in enumerate(preview_variants): + + column = index % columns + row = index // columns + + x = column * cell_width + y = row * cell_height + + preview = preview.copy() + + preview.thumbnail( + (400, 270), + Image.Resampling.NEAREST, + ) + + paste_x = ( + x + + (cell_width - preview.width) // 2 + ) + + comparison_image.paste( + preview, + (paste_x, y + 55), + ) + + if result is None: + + text = ( + f"{label}\n" + f"original: " + f"{original_size[0]}x{original_size[1]}" + ) + + else: + + text = ( + f"{label}\n" + f"{format_size(result['file_size'])}, " + f"{result['fragment_count']} fragments, " + f"{format_duration(result['times'][20])} " + f"at 20 kbps" + ) + + draw.multiline_text( + (x + 10, y + 10), + text, + fill="black", + spacing=4, + ) + + +comparison_path = ( + OUTPUT_DIRECTORY + / "lab011_comparison.jpg" +) + +comparison_image.save( + comparison_path, + format="JPEG", + quality=90, +) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print( + "=== Lab011. Режимы кадра для ровера ===" +) + +print("\nИсходное разрешение:") + +print( + original_size[0], + "x", + original_size[1], +) + +print("\nРезультаты:") + +print( + f"{'Вариант':<27}" + f"{'Размер':>12}" + f"{'Пакеты':>10}" + f"{'10 кбит/с':>13}" + f"{'20 кбит/с':>13}" + f"{'50 кбит/с':>13}" +) + +print("-" * 88) + +for result in results: + + print( + f"{result['name']:<27}" + f"{format_size(result['file_size']):>12}" + f"{result['fragment_count']:>10}" + f"{format_duration(result['times'][10]):>13}" + f"{format_duration(result['times'][20]):>13}" + f"{format_duration(result['times'][50]):>13}" + ) + + +print("\nСравнительная картинка:") + +print(comparison_path) + + +print( + "\nПроверка пройдена: " + "режимы кадра созданы и рассчитаны." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab012_color_vs_gray.py b/Projects/SDR_Rover/tests/lab012_color_vs_gray.py new file mode 100644 index 0000000..2705c4e --- /dev/null +++ b/Projects/SDR_Rover/tests/lab012_color_vs_gray.py @@ -0,0 +1,558 @@ +""" +Lab012. Сравнение цветного и серого кадров +для обнаружения человека и препятствий. + +Создаются четыре варианта: + +1. Цветной 320x240, JPEG quality 30. +2. Серый 320x240, JPEG quality 30. +3. Цветной 320x240, JPEG quality 15. +4. Серый 320x240, JPEG quality 15. + +Для каждого варианта рассчитываются: + +- размер JPEG; +- количество фрагментов; +- полный объём DATA + ACK; +- время передачи; +- стоимость сохранения цвета. +""" + +from math import ceil +from pathlib import Path + +from PIL import ( + Image, + ImageDraw, + ImageOps, +) + +from protocol.image_fragments import ( + encode_image_fragment, + split_image_bytes, +) + +from protocol.packet import ( + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +SOURCE_PATH = Path( + "data/raw/lab012_safety_scene.jpg" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab012" +) + +FRAME_SIZE = (320, 240) + +FRAGMENT_DATA_SIZE = 512 + +IMAGE_ID_BASE = 2026071500 + +BITRATES_KBPS = [ + 10, + 20, + 50, +] + + +# ============================================================ +# Вспомогательные функции +# ============================================================ + +def prepare_frame( + image: Image.Image, +) -> Image.Image: + """ + Подготовить кадр фиксированного размера 320x240. + + Изображение: + - автоматически поворачивается по EXIF; + - переводится в RGB; + - масштабируется с сохранением пропорций; + - при необходимости дополняется чёрными полями. + """ + + image = ImageOps.exif_transpose(image) + image = image.convert("RGB") + + return ImageOps.pad( + image, + FRAME_SIZE, + method=Image.Resampling.LANCZOS, + color="black", + centering=(0.5, 0.5), + ) + + +def save_jpeg( + image: Image.Image, + output_path: Path, + quality: int, +) -> None: + """ + Сохранить изображение в JPEG. + """ + + output_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + image.save( + output_path, + format="JPEG", + quality=quality, + optimize=True, + ) + + +def format_size( + byte_count: int, +) -> str: + """ + Представить размер файла в КиБ. + """ + + return f"{byte_count / 1024:.2f} КиБ" + + +def format_duration( + seconds: float, +) -> str: + """ + Представить время передачи. + """ + + if seconds < 1: + return f"{seconds * 1000:.0f} мс" + + if seconds < 60: + return f"{seconds:.2f} с" + + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + + return ( + f"{minutes} мин " + f"{remaining_seconds:.1f} с" + ) + + +def estimate_transfer( + file_path: Path, + image_id: int, +) -> dict: + """ + Рассчитать параметры передачи JPEG + с использованием текущего протокола. + """ + + image_bytes = file_path.read_bytes() + + fragments = split_image_bytes( + image_bytes=image_bytes, + image_id=image_id, + fragment_data_size=FRAGMENT_DATA_SIZE, + ) + + total_data_bytes = 0 + + for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + packet = build_packet( + payload=fragment_payload, + message_type=MESSAGE_TYPE_IMAGE_FRAGMENT, + sequence_number=fragment.fragment_index, + ) + + total_data_bytes += len(packet) + + ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=0, + ) + + total_ack_bytes = ( + len(fragments) + * len(ack_packet) + ) + + total_radio_bytes = ( + total_data_bytes + + total_ack_bytes + ) + + transfer_times = {} + + for bitrate_kbps in BITRATES_KBPS: + + transfer_times[bitrate_kbps] = ( + total_radio_bytes + * 8 + / (bitrate_kbps * 1000) + ) + + return { + "file_size": len(image_bytes), + "fragment_count": len(fragments), + "total_data_bytes": total_data_bytes, + "total_ack_bytes": total_ack_bytes, + "total_radio_bytes": total_radio_bytes, + "times": transfer_times, + } + + +# ============================================================ +# Проверка исходного файла +# ============================================================ + +if not SOURCE_PATH.exists(): + raise FileNotFoundError( + f"Не найден файл: {SOURCE_PATH}" + ) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + + +# ============================================================ +# Подготовка базового кадра +# ============================================================ + +with Image.open(SOURCE_PATH) as source_image: + + original_size = source_image.size + + color_frame = prepare_frame( + source_image + ) + +gray_frame = ImageOps.grayscale( + color_frame +) + + +# ============================================================ +# Создание вариантов +# ============================================================ + +variant_definitions = [ + { + "name": "color_320_q30", + "image": color_frame, + "quality": 30, + "path": ( + OUTPUT_DIRECTORY + / "01_color_320_q30.jpg" + ), + }, + { + "name": "gray_320_q30", + "image": gray_frame, + "quality": 30, + "path": ( + OUTPUT_DIRECTORY + / "02_gray_320_q30.jpg" + ), + }, + { + "name": "color_320_q15", + "image": color_frame, + "quality": 15, + "path": ( + OUTPUT_DIRECTORY + / "03_color_320_q15.jpg" + ), + }, + { + "name": "gray_320_q15", + "image": gray_frame, + "quality": 15, + "path": ( + OUTPUT_DIRECTORY + / "04_gray_320_q15.jpg" + ), + }, +] + + +for variant in variant_definitions: + + save_jpeg( + image=variant["image"], + output_path=variant["path"], + quality=variant["quality"], + ) + + +# ============================================================ +# Расчёт параметров передачи +# ============================================================ + +results = [] + +for variant_index, variant in enumerate( + variant_definitions +): + + transfer_result = estimate_transfer( + file_path=variant["path"], + image_id=IMAGE_ID_BASE + variant_index, + ) + + results.append( + { + "name": variant["name"], + "path": variant["path"], + "quality": variant["quality"], + **transfer_result, + } + ) + + +# ============================================================ +# Создание сравнительной картинки +# ============================================================ + +columns = 2 +cell_width = 440 +cell_height = 340 + +rows = ceil( + len(results) + / columns +) + +comparison_image = Image.new( + "RGB", + ( + columns * cell_width, + rows * cell_height, + ), + "white", +) + +draw = ImageDraw.Draw( + comparison_image +) + + +for result_index, result in enumerate(results): + + column = result_index % columns + row = result_index // columns + + cell_x = column * cell_width + cell_y = row * cell_height + + with Image.open(result["path"]) as image: + + preview = image.convert("RGB") + + # NEAREST не сглаживает пиксели при увеличении. + # Поэтому дефекты сжатия будут хорошо видны. + preview = preview.resize( + (400, 300), + Image.Resampling.NEAREST, + ) + + paste_x = ( + cell_x + + (cell_width - preview.width) // 2 + ) + + paste_y = cell_y + 35 + + comparison_image.paste( + preview, + (paste_x, paste_y), + ) + + label = ( + f"{result['name']}\n" + f"{format_size(result['file_size'])}, " + f"{result['fragment_count']} fragments, " + f"{format_duration(result['times'][20])} " + f"at 20 kbps" + ) + + draw.multiline_text( + (cell_x + 10, cell_y + 5), + label, + fill="black", + spacing=3, + ) + + +comparison_path = ( + OUTPUT_DIRECTORY + / "lab012_comparison.jpg" +) + +comparison_image.save( + comparison_path, + format="JPEG", + quality=92, +) + + +# ============================================================ +# Поиск результатов по имени +# ============================================================ + +results_by_name = { + result["name"]: result + for result in results +} + +color_q30 = results_by_name[ + "color_320_q30" +] + +gray_q30 = results_by_name[ + "gray_320_q30" +] + +color_q15 = results_by_name[ + "color_320_q15" +] + +gray_q15 = results_by_name[ + "gray_320_q15" +] + + +# ============================================================ +# Расчёт цены сохранения цвета +# ============================================================ + +color_overhead_q30_bytes = ( + color_q30["file_size"] + - gray_q30["file_size"] +) + +color_overhead_q30_percent = ( + color_overhead_q30_bytes + / gray_q30["file_size"] + * 100 +) + +color_overhead_q15_bytes = ( + color_q15["file_size"] + - gray_q15["file_size"] +) + +color_overhead_q15_percent = ( + color_overhead_q15_bytes + / gray_q15["file_size"] + * 100 +) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print( + "=== Lab012. Цветной или серый кадр ===" +) + +print("\nИсходное разрешение:") + +print( + original_size[0], + "x", + original_size[1], +) + + +print("\nСравнение вариантов:") + +print( + f"{'Вариант':<22}" + f"{'Размер':>12}" + f"{'Пакеты':>10}" + f"{'10 кбит/с':>13}" + f"{'20 кбит/с':>13}" + f"{'50 кбит/с':>13}" +) + +print("-" * 83) + + +for result in results: + + print( + f"{result['name']:<22}" + f"{format_size(result['file_size']):>12}" + f"{result['fragment_count']:>10}" + f"{format_duration(result['times'][10]):>13}" + f"{format_duration(result['times'][20]):>13}" + f"{format_duration(result['times'][50]):>13}" + ) + + +print("\nЦена сохранения цвета при quality 30:") + +print( + format_size( + color_overhead_q30_bytes + ), + f"или {color_overhead_q30_percent:.1f} %", +) + + +print("\nЦена сохранения цвета при quality 15:") + +print( + format_size( + color_overhead_q15_bytes + ), + f"или {color_overhead_q15_percent:.1f} %", +) + + +print("\nСравнительная картинка:") + +print(comparison_path) + + +# ============================================================ +# Проверки +# ============================================================ + +assert len(results) == 4 + +assert comparison_path.exists() + +assert all( + result["fragment_count"] > 0 + for result in results +) + +assert color_q30["file_size"] > 0 +assert gray_q30["file_size"] > 0 +assert color_q15["file_size"] > 0 +assert gray_q15["file_size"] > 0 + + +print( + "\nПроверка пройдена: " + "цветные и серые варианты созданы и рассчитаны." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab013_bpsk_python.py b/Projects/SDR_Rover/tests/lab013_bpsk_python.py new file mode 100644 index 0000000..b6c723b --- /dev/null +++ b/Projects/SDR_Rover/tests/lab013_bpsk_python.py @@ -0,0 +1,528 @@ +""" +Lab013. Первая BPSK-передача цифрового пакета в Python. + +Программа: +1. Формирует пакет SDR Rover Link с CRC-32. +2. Преобразует байты пакета в отдельные биты. +3. Преобразует биты в BPSK-символы. +4. Представляет символы как комплексные IQ-сэмплы. +5. Добавляет комплексный белый гауссов шум. +6. Демодулирует BPSK. +7. Восстанавливает байты пакета. +8. Проверяет пакет и CRC. +9. Сравнивает результат при разных уровнях SNR. +""" + +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_TEXT, + PacketError, + build_packet, + parse_packet, +) + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +MESSAGE = "ПРИВЕТ SDR" + +SEQUENCE_NUMBER = 13 + +# Проверим несколько уровней отношения сигнал/шум. +SNR_VALUES_DB = [ + 12.0, + 6.0, + 2.0, + 0.0, +] + +# Фиксированное значение обеспечивает повторяемость. +RANDOM_SEED = 2026 + +OUTPUT_DIRECTORY = Path( + "data/processed/lab013" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + + +# ============================================================ +# Преобразование байтов в биты +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать последовательность bytes + в массив отдельных битов 0 и 1. + + Каждый исходный байт превращается в восемь битов. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + byte_array = np.frombuffer( + data, + dtype=np.uint8, + ) + + bits = np.unpackbits( + byte_array + ) + + return bits + + +# ============================================================ +# Преобразование битов обратно в байты +# ============================================================ + +def bits_to_bytes( + bits: np.ndarray, +) -> bytes: + """ + Упаковать отдельные биты обратно в bytes. + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if len(bits) % 8 != 0: + raise ValueError( + "Количество битов должно быть кратно восьми" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "Массив должен содержать только 0 и 1" + ) + + packed_bytes = np.packbits( + bits + ) + + return packed_bytes.tobytes() + + +# ============================================================ +# BPSK-модулятор +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в комплексные BPSK-символы. + + Используем отображение: + + 0 → -1 + 0j + 1 → +1 + 0j + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + real_symbols = ( + 2.0 * bits.astype(np.float64) + - 1.0 + ) + + iq_symbols = real_symbols.astype( + np.complex128 + ) + + return iq_symbols + + +# ============================================================ +# Модель радиошума +# ============================================================ + +def add_awgn( + iq_samples: np.ndarray, + snr_db: float, + random_generator: np.random.Generator, +) -> np.ndarray: + """ + Добавить комплексный белый гауссов шум AWGN. + + AWGN: + Additive White Gaussian Noise — + аддитивный белый гауссов шум. + + snr_db: + Отношение средней мощности сигнала + к средней мощности шума в децибелах. + """ + + signal_power = np.mean( + np.abs(iq_samples) ** 2 + ) + + snr_linear = 10.0 ** ( + snr_db / 10.0 + ) + + noise_power = ( + signal_power / snr_linear + ) + + # Комплексный шум имеет две составляющие: + # действительную I и мнимую Q. + # + # Поэтому мощность делится между ними пополам. + noise_sigma = np.sqrt( + noise_power / 2.0 + ) + + noise = noise_sigma * ( + random_generator.standard_normal( + len(iq_samples) + ) + + 1j + * random_generator.standard_normal( + len(iq_samples) + ) + ) + + return iq_samples + noise + + +# ============================================================ +# BPSK-демодулятор +# ============================================================ + +def bpsk_demodulate( + received_iq: np.ndarray, +) -> np.ndarray: + """ + Преобразовать принятые IQ-сэмплы в биты. + + Правило решения: + + I < 0 → бит 0 + I >= 0 → бит 1 + """ + + received_bits = ( + received_iq.real >= 0.0 + ).astype(np.uint8) + + return received_bits + + +# ============================================================ +# Формирование исходного пакета +# ============================================================ + +payload = MESSAGE.encode( + "utf-8" +) + +original_packet = build_packet( + payload=payload, + message_type=MESSAGE_TYPE_TEXT, + sequence_number=SEQUENCE_NUMBER, +) + +transmitted_bits = bytes_to_bits( + original_packet +) + +transmitted_iq = bpsk_modulate( + transmitted_bits +) + + +print( + "=== Lab013. BPSK в Python ===" +) + +print("\nИсходное сообщение:") +print(MESSAGE) + +print("\nРазмер пакета:") +print( + len(original_packet), + "байт", +) + +print("\nКоличество передаваемых битов:") +print( + len(transmitted_bits) +) + +print("\nПервые 32 бита:") + +print( + " ".join( + str(bit) + for bit in transmitted_bits[:32] + ) +) + +print("\nПервые 16 BPSK-символов:") + +print( + transmitted_iq[:16] +) + + +# ============================================================ +# Передача при разных уровнях SNR +# ============================================================ + +experiment_results = [] + +constellation_samples = {} + +for experiment_index, snr_db in enumerate( + SNR_VALUES_DB +): + + random_generator = np.random.default_rng( + RANDOM_SEED + experiment_index + ) + + received_iq = add_awgn( + iq_samples=transmitted_iq, + snr_db=snr_db, + random_generator=random_generator, + ) + + received_bits = bpsk_demodulate( + received_iq + ) + + bit_error_count = int( + np.count_nonzero( + transmitted_bits != received_bits + ) + ) + + bit_error_rate = ( + bit_error_count + / len(transmitted_bits) + ) + + received_packet = bits_to_bytes( + received_bits + ) + + packet_status = "CRC OK" + restored_message = None + + try: + parsed_packet = parse_packet( + received_packet + ) + + restored_message = ( + parsed_packet.payload.decode( + "utf-8" + ) + ) + + except CRCError: + packet_status = "CRC ERROR" + + except PacketError: + packet_status = "PACKET ERROR" + + except UnicodeDecodeError: + packet_status = "UTF-8 ERROR" + + experiment_results.append( + { + "snr_db": snr_db, + "bit_errors": bit_error_count, + "ber": bit_error_rate, + "packet_status": packet_status, + "restored_message": restored_message, + } + ) + + # Для графика достаточно первых 300 символов. + constellation_samples[snr_db] = ( + received_iq[:300] + ) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print("\nРезультаты передачи:") + +print( + f"{'SNR':>8}" + f"{'Ошибки битов':>16}" + f"{'BER':>14}" + f"{'Пакет':>18}" +) + +print("-" * 56) + +for result in experiment_results: + + print( + f"{result['snr_db']:>6.1f} дБ" + f"{result['bit_errors']:>16}" + f"{result['ber']:>14.6f}" + f"{result['packet_status']:>18}" + ) + + +print("\nВосстановленные сообщения:") + +for result in experiment_results: + + print( + f"SNR {result['snr_db']:>4.1f} дБ: " + f"{result['restored_message']}" + ) + + +# ============================================================ +# График созвездия +# ============================================================ + +figure, axes = plt.subplots( + 2, + 2, + figsize=(10, 8), +) + +axes = axes.ravel() + +for axis, snr_db in zip( + axes, + SNR_VALUES_DB, +): + + received_iq = constellation_samples[ + snr_db + ] + + axis.scatter( + received_iq.real, + received_iq.imag, + s=12, + alpha=0.6, + ) + + axis.axvline( + 0.0, + linewidth=1, + ) + + axis.set_title( + f"BPSK, SNR = {snr_db:.1f} дБ" + ) + + axis.set_xlabel( + "I — синфазная компонента" + ) + + axis.set_ylabel( + "Q — квадратурная компонента" + ) + + axis.set_xlim( + -2.5, + 2.5, + ) + + axis.set_ylim( + -1.8, + 1.8, + ) + + axis.grid( + True + ) + +figure.tight_layout() + +constellation_path = ( + OUTPUT_DIRECTORY + / "lab013_bpsk_constellation.png" +) + +figure.savefig( + constellation_path, + dpi=150, +) + +plt.close( + figure +) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +# Без добавления шума преобразование должно быть обратимым. +ideal_received_bits = bpsk_demodulate( + transmitted_iq +) + +ideal_received_packet = bits_to_bytes( + ideal_received_bits +) + +assert ideal_received_packet == original_packet + +ideal_parsed_packet = parse_packet( + ideal_received_packet +) + +assert ( + ideal_parsed_packet.payload.decode( + "utf-8" + ) + == MESSAGE +) + +assert len(experiment_results) == len( + SNR_VALUES_DB +) + +assert constellation_path.exists() + + +print("\nГрафик созвездия:") +print(constellation_path) + +print( + "\nПроверка пройдена: " + "пакет преобразован в BPSK IQ-сэмплы " + "и демодулирован обратно." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab014_bpsk_ber_curve.py b/Projects/SDR_Rover/tests/lab014_bpsk_ber_curve.py new file mode 100644 index 0000000..97d3da2 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab014_bpsk_ber_curve.py @@ -0,0 +1,617 @@ +""" +Lab014. Измерение BER для BPSK в канале AWGN. + +Программа: + +1. Генерирует 1 000 000 случайных битов. +2. Преобразует их в BPSK-символы. +3. Добавляет комплексный гауссов шум. +4. Демодулирует принятый сигнал. +5. Измеряет экспериментальный BER. +6. Рассчитывает теоретический BER. +7. Оценивает вероятность повреждения пакетов разной длины. +8. Строит BER-кривую. +9. Сохраняет результаты в CSV. + +Упрощения модели: + +- один бит передаётся одним BPSK-символом; +- один символ представлен одним IQ-сэмплом; +- частотная и фазовая синхронизация идеальны; +- межсимвольные искажения отсутствуют; +- канал содержит только AWGN. +""" + +from csv import DictWriter +from math import ( + erfc, + expm1, + log1p, + sqrt, +) +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +BIT_COUNT = 1_000_000 + +EB_N0_VALUES_DB = [ + -4.0, + -2.0, + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, +] + +RANDOM_SEED = 2026 + +OUTPUT_DIRECTORY = Path( + "data/processed/lab014" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab014_bpsk_ber_curve.png" +) + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab014_bpsk_ber_results.csv" +) + + +# ============================================================ +# Размеры пакетов для оценки PER +# ============================================================ + +# Пакет из Lab013: +# +# заголовок 8 байт +# PAYLOAD 16 байт +# CRC 4 байта +# +# Итого 28 байт = 224 бита +SHORT_PACKET_BITS = 224 + +# Типовой пакет изображения: +# +# заголовок протокола 8 байт +# заголовок фрагмента 12 байт +# данные JPEG 512 байт +# CRC 4 байта +# +# Итого 536 байт +IMAGE_PACKET_BITS = 536 * 8 + + +# ============================================================ +# Вспомогательные функции +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в BPSK-символы. + + Отображение: + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + symbols = ( + 2.0 * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +def add_awgn( + iq_samples: np.ndarray, + eb_n0_db: float, + random_generator: np.random.Generator, +) -> np.ndarray: + """ + Добавить комплексный AWGN-шум. + + Энергия BPSK-символа равна единице. + + Для одного бита на символ: + + Es/N0 = Eb/N0 + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + # Для комплексного AWGN каждая компонента + # I и Q получает половину полной мощности шума. + noise_sigma = sqrt( + 1.0 + / ( + 2.0 + * eb_n0_linear + ) + ) + + noise = noise_sigma * ( + random_generator.standard_normal( + len(iq_samples) + ) + + 1j + * random_generator.standard_normal( + len(iq_samples) + ) + ) + + return iq_samples + noise + + +def bpsk_demodulate( + received_iq: np.ndarray, +) -> np.ndarray: + """ + Демодулировать BPSK по знаку компоненты I. + + I < 0 → бит 0 + I >= 0 → бит 1 + """ + + return ( + received_iq.real >= 0.0 + ).astype(np.uint8) + + +def theoretical_bpsk_ber( + eb_n0_db: float, +) -> float: + """ + Рассчитать теоретический BER когерентной BPSK + в канале AWGN. + + BER = 0.5 * erfc(sqrt(Eb/N0)) + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + return 0.5 * erfc( + sqrt(eb_n0_linear) + ) + + +def ber_to_per( + ber: float, + packet_bit_count: int, +) -> float: + """ + Оценить Packet Error Rate из BER. + + Предполагается: + + - ошибки отдельных битов независимы; + - пакет считается повреждённым, + если ошибся хотя бы один бит. + + PER = 1 - (1 - BER) ** N + """ + + if not 0.0 <= ber <= 1.0: + raise ValueError( + "BER должен находиться в диапазоне 0...1" + ) + + if packet_bit_count <= 0: + raise ValueError( + "packet_bit_count должен быть положительным" + ) + + if ber == 0.0: + return 0.0 + + if ber == 1.0: + return 1.0 + + # Такая запись численно устойчивее, + # чем прямое возведение в степень + # для очень маленьких BER. + return -expm1( + packet_bit_count + * log1p(-ber) + ) + + +# ============================================================ +# Генерация исходной последовательности +# ============================================================ + +bit_generator = np.random.default_rng( + RANDOM_SEED +) + +transmitted_bits = bit_generator.integers( + low=0, + high=2, + size=BIT_COUNT, + dtype=np.uint8, +) + +transmitted_iq = bpsk_modulate( + transmitted_bits +) + + +# ============================================================ +# Эксперимент при разных Eb/N0 +# ============================================================ + +results = [] + +for experiment_index, eb_n0_db in enumerate( + EB_N0_VALUES_DB +): + + noise_generator = np.random.default_rng( + RANDOM_SEED + + 1000 + + experiment_index + ) + + received_iq = add_awgn( + iq_samples=transmitted_iq, + eb_n0_db=eb_n0_db, + random_generator=noise_generator, + ) + + received_bits = bpsk_demodulate( + received_iq + ) + + bit_error_count = int( + np.count_nonzero( + transmitted_bits + != received_bits + ) + ) + + experimental_ber = ( + bit_error_count + / BIT_COUNT + ) + + theoretical_ber = theoretical_bpsk_ber( + eb_n0_db + ) + + short_packet_per = ber_to_per( + theoretical_ber, + SHORT_PACKET_BITS, + ) + + image_packet_per = ber_to_per( + theoretical_ber, + IMAGE_PACKET_BITS, + ) + + results.append( + { + "eb_n0_db": eb_n0_db, + "bit_errors": bit_error_count, + "experimental_ber": experimental_ber, + "theoretical_ber": theoretical_ber, + "short_packet_per": short_packet_per, + "image_packet_per": image_packet_per, + } + ) + + +# ============================================================ +# Вывод основных результатов +# ============================================================ + +print( + "=== Lab014. BER-кривая BPSK ===" +) + +print("\nКоличество переданных битов:") + +print( + f"{BIT_COUNT:,}".replace(",", " ") +) + +print("\nРезультаты:") + +print( + f"{'Eb/N0':>9}" + f"{'Ошибки':>12}" + f"{'BER эксперимент':>19}" + f"{'BER теория':>16}" + f"{'PER 224 бит':>16}" + f"{'PER 4288 бит':>17}" +) + +print("-" * 89) + +for result in results: + + print( + f"{result['eb_n0_db']:>6.1f} дБ" + f"{result['bit_errors']:>12}" + f"{result['experimental_ber']:>19.6e}" + f"{result['theoretical_ber']:>16.6e}" + f"{result['short_packet_per']:>16.6f}" + f"{result['image_packet_per']:>17.6f}" + ) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + fieldnames = [ + "eb_n0_db", + "bit_errors", + "experimental_ber", + "theoretical_ber", + "short_packet_per_224_bits", + "image_packet_per_4288_bits", + ] + + writer = DictWriter( + csv_file, + fieldnames=fieldnames, + ) + + writer.writeheader() + + for result in results: + + writer.writerow( + { + "eb_n0_db": result["eb_n0_db"], + "bit_errors": result["bit_errors"], + "experimental_ber": ( + result["experimental_ber"] + ), + "theoretical_ber": ( + result["theoretical_ber"] + ), + "short_packet_per_224_bits": ( + result["short_packet_per"] + ), + "image_packet_per_4288_bits": ( + result["image_packet_per"] + ), + } + ) + + +# ============================================================ +# Подготовка данных для графика +# ============================================================ + +eb_n0_plot_values = np.array( + [ + result["eb_n0_db"] + for result in results + ], + dtype=np.float64, +) + +experimental_ber_values = np.array( + [ + result["experimental_ber"] + for result in results + ], + dtype=np.float64, +) + +theoretical_ber_values = np.array( + [ + result["theoretical_ber"] + for result in results + ], + dtype=np.float64, +) + + +# Нулевой измеренный BER невозможно показать +# на логарифмической шкале. +# +# Поэтому для графика ставим такую точку +# на уровень половины одного наблюдаемого события. +measurement_floor = ( + 0.5 / BIT_COUNT +) + +experimental_ber_for_plot = np.maximum( + experimental_ber_values, + measurement_floor, +) + + +# ============================================================ +# Построение BER-графика +# ============================================================ + +figure = plt.figure( + figsize=(10, 7) +) + +plt.semilogy( + eb_n0_plot_values, + theoretical_ber_values, + marker="o", + label="Теоретический BER BPSK", +) + +plt.semilogy( + eb_n0_plot_values, + experimental_ber_for_plot, + marker="s", + linestyle="--", + label="Экспериментальный BER", +) + +plt.axhline( + measurement_floor, + linestyle=":", + label=( + "Предел измерения " + f"{measurement_floor:.1e}" + ), +) + +plt.xlabel( + "Eb/N0, дБ" +) + +plt.ylabel( + "BER" +) + +plt.title( + "BPSK в канале AWGN: " + "эксперимент и теория" +) + +plt.grid( + True, + which="both", +) + +plt.legend() + +plt.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Инженерные контрольные точки +# ============================================================ + +print("\nИнженерные контрольные точки:") + +for target_db in [ + 6.0, + 8.0, + 10.0, +]: + + result = next( + item + for item in results + if item["eb_n0_db"] == target_db + ) + + print( + f"\nEb/N0 = {target_db:.1f} дБ" + ) + + print( + "Теоретический BER:", + f"{result['theoretical_ber']:.6e}", + ) + + print( + "PER короткого пакета 224 бита:", + f"{result['short_packet_per'] * 100:.2f} %", + ) + + print( + "PER JPEG-пакета 4288 бит:", + f"{result['image_packet_per'] * 100:.2f} %", + ) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert len(transmitted_bits) == BIT_COUNT + +assert len(received_bits) == BIT_COUNT + +assert all( + 0.0 + <= result["experimental_ber"] + <= 1.0 + for result in results +) + +assert all( + 0.0 + <= result["theoretical_ber"] + <= 1.0 + for result in results +) + +assert GRAPH_PATH.exists() + +assert CSV_PATH.exists() + + +print("\nГрафик BER:") + +print(GRAPH_PATH) + +print("\nТаблица CSV:") + +print(CSV_PATH) + +print( + "\nПроверка пройдена: " + "экспериментальная BER-кривая построена." +) diff --git a/Projects/SDR_Rover/tests/lab015_fragment_size_optimization.py b/Projects/SDR_Rover/tests/lab015_fragment_size_optimization.py new file mode 100644 index 0000000..2846cb6 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab015_fragment_size_optimization.py @@ -0,0 +1,810 @@ +""" +Lab015. Подбор оптимального размера фрагмента изображения. + +Программа сравнивает фрагменты размером: + +- 64 байта; +- 128 байт; +- 256 байт; +- 512 байт; +- 1024 байта. + +Для каждого размера рассчитываются: + +1. Количество фрагментов изображения. +2. Служебные расходы протокола. +3. Теоретический BER BPSK. +4. Вероятность успешной доставки DATA-пакета. +5. Вероятность успешной доставки ACK. +6. Среднее число передач одного фрагмента. +7. Ожидаемый полный радиообмен с ARQ. +8. Время передачи фотографии при 20 кбит/с. +9. Эффективная полезная скорость. + +Модель канала: + +- когерентная BPSK; +- AWGN; +- ошибки битов независимы; +- повреждённый пакет отбрасывается по CRC; +- применяется Stop-and-Wait ARQ; +- DATA и ACK работают при одинаковом Eb/N0. + +Не учитываются: + +- ожидание тайм-аута; +- паузы между DATA и ACK; +- преамбула; +- синхронизация; +- FEC; +- многолучёвость; +- частотные и фазовые ошибки. +""" + +from csv import DictWriter +from math import ( + erfc, + exp, + log1p, + sqrt, +) +from pathlib import Path + +import matplotlib.pyplot as plt + +from protocol.image_fragments import ( + encode_image_fragment, + split_image_bytes, +) + +from protocol.packet import ( + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +SOURCE_PATH = Path( + "data/raw/lab009_source.jpg" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab015" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +FRAGMENT_SIZES = [ + 64, + 128, + 256, + 512, + 1024, +] + +EB_N0_VALUES_DB = [ + 6.0, + 8.0, + 10.0, + 12.0, +] + +CHANNEL_BITRATE_BPS = 20_000 + +IMAGE_ID_BASE = 2026071600 + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab015_fragment_results.csv" +) + +TIME_GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab015_transfer_time.png" +) + +GOODPUT_GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab015_effective_goodput.png" +) + + +# ============================================================ +# Вспомогательные функции +# ============================================================ + +def theoretical_bpsk_ber( + eb_n0_db: float, +) -> float: + """ + Теоретический BER когерентной BPSK в AWGN. + + BER = 0.5 * erfc(sqrt(Eb/N0)) + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + return 0.5 * erfc( + sqrt(eb_n0_linear) + ) + + +def packet_success_probability( + ber: float, + packet_bit_count: int, +) -> float: + """ + Вероятность того, что весь пакет будет принят + без единой битовой ошибки. + + P_success = (1 - BER) ** N + """ + + if not 0.0 <= ber <= 1.0: + raise ValueError( + "BER должен находиться в диапазоне 0...1" + ) + + if packet_bit_count <= 0: + raise ValueError( + "Размер пакета должен быть положительным" + ) + + if ber == 0.0: + return 1.0 + + if ber == 1.0: + return 0.0 + + # Численно устойчивый вариант выражения: + # + # (1 - BER) ** N + return exp( + packet_bit_count + * log1p(-ber) + ) + + +def format_bytes( + byte_count: float, +) -> str: + """ + Представить объём в удобном виде. + """ + + if byte_count < 1024: + return f"{byte_count:.0f} байт" + + kibibytes = byte_count / 1024 + + if kibibytes < 1024: + return f"{kibibytes:.2f} КиБ" + + mebibytes = kibibytes / 1024 + + return f"{mebibytes:.2f} МиБ" + + +def format_duration( + seconds: float, +) -> str: + """ + Представить длительность в удобном виде. + """ + + if seconds < 1: + return f"{seconds * 1000:.0f} мс" + + if seconds < 60: + return f"{seconds:.2f} с" + + if seconds < 3600: + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + + return ( + f"{minutes} мин " + f"{remaining_seconds:.1f} с" + ) + + hours = int(seconds // 3600) + remaining_minutes = ( + seconds % 3600 + ) / 60 + + return ( + f"{hours} ч " + f"{remaining_minutes:.1f} мин" + ) + + +# ============================================================ +# Проверка исходного JPEG +# ============================================================ + +if not SOURCE_PATH.exists(): + raise FileNotFoundError( + f"Не найден файл: {SOURCE_PATH}. " + "Сначала необходимо выполнить Lab009." + ) + +source_bytes = SOURCE_PATH.read_bytes() + +if not source_bytes: + raise ValueError( + "Исходный JPEG пуст" + ) + +source_size = len(source_bytes) + + +# ============================================================ +# Размер ACK +# ============================================================ + +ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=0, +) + +ack_packet_size_bytes = len( + ack_packet +) + +ack_packet_size_bits = ( + ack_packet_size_bytes * 8 +) + + +# ============================================================ +# Основной расчёт +# ============================================================ + +results = [] + +for fragment_size_index, fragment_size in enumerate( + FRAGMENT_SIZES +): + + fragments = split_image_bytes( + image_bytes=source_bytes, + image_id=( + IMAGE_ID_BASE + + fragment_size_index + ), + fragment_data_size=fragment_size, + ) + + data_packet_sizes_bytes = [] + + for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + data_packet = build_packet( + payload=fragment_payload, + message_type=( + MESSAGE_TYPE_IMAGE_FRAGMENT + ), + sequence_number=( + fragment.fragment_index + ), + ) + + data_packet_sizes_bytes.append( + len(data_packet) + ) + + ideal_data_bytes = sum( + data_packet_sizes_bytes + ) + + ideal_ack_bytes = ( + len(fragments) + * ack_packet_size_bytes + ) + + ideal_total_bytes = ( + ideal_data_bytes + + ideal_ack_bytes + ) + + ideal_efficiency_percent = ( + source_size + / ideal_total_bytes + * 100 + ) + + for eb_n0_db in EB_N0_VALUES_DB: + + ber = theoretical_bpsk_ber( + eb_n0_db + ) + + ack_success_probability = ( + packet_success_probability( + ber=ber, + packet_bit_count=( + ack_packet_size_bits + ), + ) + ) + + expected_data_bytes = 0.0 + expected_ack_bytes = 0.0 + + expected_data_transmissions = 0.0 + + data_success_probabilities = [] + + for data_packet_size_bytes in ( + data_packet_sizes_bytes + ): + + data_packet_size_bits = ( + data_packet_size_bytes * 8 + ) + + data_success_probability = ( + packet_success_probability( + ber=ber, + packet_bit_count=( + data_packet_size_bits + ), + ) + ) + + data_success_probabilities.append( + data_success_probability + ) + + # Для успешного завершения попытки должны + # одновременно правильно пройти DATA и ACK. + confirmed_attempt_probability = ( + data_success_probability + * ack_success_probability + ) + + if confirmed_attempt_probability == 0.0: + raise RuntimeError( + "Вероятность подтверждения " + "оказалась равной нулю" + ) + + # Число DATA-передач до успешного ACK + # подчиняется геометрическому распределению. + expected_attempt_count = ( + 1.0 + / confirmed_attempt_probability + ) + + expected_data_transmissions += ( + expected_attempt_count + ) + + expected_data_bytes += ( + data_packet_size_bytes + * expected_attempt_count + ) + + # ACK передаётся только после правильно + # принятого DATA. + # + # Среднее количество передач ACK до + # успешного ACK равно 1 / P_ACK. + expected_ack_bytes += ( + ack_packet_size_bytes + / ack_success_probability + ) + + expected_total_bytes = ( + expected_data_bytes + + expected_ack_bytes + ) + + expected_transfer_seconds = ( + expected_total_bytes + * 8 + / CHANNEL_BITRATE_BPS + ) + + expected_efficiency_percent = ( + source_size + / expected_total_bytes + * 100 + ) + + effective_goodput_bps = ( + source_size + * 8 + / expected_transfer_seconds + ) + + average_data_transmissions = ( + expected_data_transmissions + / len(fragments) + ) + + minimum_data_success_probability = min( + data_success_probabilities + ) + + maximum_data_success_probability = max( + data_success_probabilities + ) + + results.append( + { + "fragment_size": fragment_size, + "fragment_count": len(fragments), + "eb_n0_db": eb_n0_db, + "ber": ber, + "ideal_total_bytes": ( + ideal_total_bytes + ), + "ideal_efficiency_percent": ( + ideal_efficiency_percent + ), + "ack_success_probability": ( + ack_success_probability + ), + "minimum_data_success_probability": ( + minimum_data_success_probability + ), + "maximum_data_success_probability": ( + maximum_data_success_probability + ), + "average_data_transmissions": ( + average_data_transmissions + ), + "expected_total_bytes": ( + expected_total_bytes + ), + "expected_transfer_seconds": ( + expected_transfer_seconds + ), + "expected_efficiency_percent": ( + expected_efficiency_percent + ), + "effective_goodput_bps": ( + effective_goodput_bps + ), + } + ) + + +# ============================================================ +# Вывод общих данных +# ============================================================ + +print( + "=== Lab015. Оптимальный размер фрагмента ===" +) + +print("\nИсходный JPEG:") + +print(SOURCE_PATH) + +print("\nРазмер JPEG:") + +print( + format_bytes(source_size) +) + +print("\nСкорость физического канала:") + +print( + CHANNEL_BITRATE_BPS / 1000, + "кбит/с", +) + +print("\nРазмер ACK-пакета:") + +print( + ack_packet_size_bytes, + "байт", +) + + +# ============================================================ +# Вывод результатов отдельно для каждого Eb/N0 +# ============================================================ + +for eb_n0_db in EB_N0_VALUES_DB: + + selected_results = [ + result + for result in results + if result["eb_n0_db"] == eb_n0_db + ] + + print( + f"\n--- Eb/N0 = {eb_n0_db:.1f} дБ ---" + ) + + print( + f"{'Фрагмент':>10}" + f"{'Кол-во':>9}" + f"{'P DATA':>12}" + f"{'Попыток':>11}" + f"{'Объём с ARQ':>16}" + f"{'Время':>16}" + f"{'Goodput':>13}" + ) + + print("-" * 87) + + for result in selected_results: + + print( + f"{result['fragment_size']:>8} Б" + f"{result['fragment_count']:>9}" + f"{result['minimum_data_success_probability']:>12.6f}" + f"{result['average_data_transmissions']:>11.2f}" + f"{format_bytes(result['expected_total_bytes']):>16}" + f"{format_duration(result['expected_transfer_seconds']):>16}" + f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с" + ) + + optimum_result = min( + selected_results, + key=lambda item: ( + item["expected_transfer_seconds"] + ), + ) + + print( + "\nОптимальный размер фрагмента:" + ) + + print( + optimum_result["fragment_size"], + "байт", + ) + + print( + "Ожидаемое время:", + format_duration( + optimum_result[ + "expected_transfer_seconds" + ] + ), + ) + + print( + "Полезная скорость:", + f"{optimum_result['effective_goodput_bps'] / 1000:.2f}", + "кбит/с", + ) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + fieldnames = [ + "fragment_size", + "fragment_count", + "eb_n0_db", + "ber", + "ideal_total_bytes", + "ideal_efficiency_percent", + "ack_success_probability", + "minimum_data_success_probability", + "maximum_data_success_probability", + "average_data_transmissions", + "expected_total_bytes", + "expected_transfer_seconds", + "expected_efficiency_percent", + "effective_goodput_bps", + ] + + writer = DictWriter( + csv_file, + fieldnames=fieldnames, + ) + + writer.writeheader() + + writer.writerows(results) + + +# ============================================================ +# График ожидаемого времени передачи +# ============================================================ + +plt.figure( + figsize=(10, 7) +) + +for eb_n0_db in EB_N0_VALUES_DB: + + selected_results = [ + result + for result in results + if result["eb_n0_db"] == eb_n0_db + ] + + fragment_sizes = [ + result["fragment_size"] + for result in selected_results + ] + + transfer_times = [ + result["expected_transfer_seconds"] + for result in selected_results + ] + + plt.plot( + fragment_sizes, + transfer_times, + marker="o", + label=f"Eb/N0 = {eb_n0_db:.0f} дБ", + ) + +plt.xscale( + "log", + base=2, +) + +plt.yscale( + "log", +) + +plt.xlabel( + "Размер данных фрагмента, байт" +) + +plt.ylabel( + "Ожидаемое время передачи, с" +) + +plt.title( + "Влияние размера фрагмента " + "на время передачи JPEG" +) + +plt.grid( + True, + which="both", +) + +plt.legend() + +plt.tight_layout() + +plt.savefig( + TIME_GRAPH_PATH, + dpi=160, +) + +plt.close() + + +# ============================================================ +# График эффективной полезной скорости +# ============================================================ + +plt.figure( + figsize=(10, 7) +) + +for eb_n0_db in EB_N0_VALUES_DB: + + selected_results = [ + result + for result in results + if result["eb_n0_db"] == eb_n0_db + ] + + fragment_sizes = [ + result["fragment_size"] + for result in selected_results + ] + + goodput_values = [ + result["effective_goodput_bps"] / 1000 + for result in selected_results + ] + + plt.plot( + fragment_sizes, + goodput_values, + marker="o", + label=f"Eb/N0 = {eb_n0_db:.0f} дБ", + ) + +plt.xscale( + "log", + base=2, +) + +plt.xlabel( + "Размер данных фрагмента, байт" +) + +plt.ylabel( + "Полезная скорость, кбит/с" +) + +plt.title( + "Полезная скорость JPEG " + "с учётом CRC и ARQ" +) + +plt.grid( + True, + which="both", +) + +plt.legend() + +plt.tight_layout() + +plt.savefig( + GOODPUT_GRAPH_PATH, + dpi=160, +) + +plt.close() + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert results + +assert CSV_PATH.exists() + +assert TIME_GRAPH_PATH.exists() + +assert GOODPUT_GRAPH_PATH.exists() + +assert all( + result["expected_transfer_seconds"] > 0 + for result in results +) + +assert all( + 0.0 + < result["ack_success_probability"] + <= 1.0 + for result in results +) + + +print("\nCSV:") + +print(CSV_PATH) + +print("\nГрафик времени:") + +print(TIME_GRAPH_PATH) + +print("\nГрафик полезной скорости:") + +print(GOODPUT_GRAPH_PATH) + +print( + "\nПроверка пройдена: " + "оптимальный размер фрагмента рассчитан." +) diff --git a/Projects/SDR_Rover/tests/lab016_adaptive_fragment_size.py b/Projects/SDR_Rover/tests/lab016_adaptive_fragment_size.py new file mode 100644 index 0000000..17a9de4 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab016_adaptive_fragment_size.py @@ -0,0 +1,605 @@ +""" +Lab016. Адаптивный выбор размера фрагмента изображения. + +Программа имитирует изменение качества канала во времени. + +Для каждой оценки Eb/N0 передатчик решает: + +- отключить изображения; +- использовать 128 байт; +- использовать 512 байт; +- использовать 1024 байта. + +Дополнительно рассчитывается ожидаемое время передачи +реального JPEG-файла. +""" + +from csv import DictWriter +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +from protocol.image_fragments import ( + encode_image_fragment, + split_image_bytes, +) + +from protocol.link_adaptation import ( + choose_image_mode, + packet_success_probability, +) + +from protocol.packet import ( + MESSAGE_TYPE_ACK, + MESSAGE_TYPE_IMAGE_FRAGMENT, + build_packet, +) + + +# ============================================================ +# Настройки +# ============================================================ + +CHANNEL_BITRATE_BPS = 20_000 + +MAX_ATTEMPTS = 5 + +CANDIDATE_FRAGMENT_SIZES = ( + 128, + 512, + 1024, +) + +# Изменение качества канала во времени. +EB_N0_PROFILE_DB = [ + 12.0, + 10.0, + 9.0, + 8.0, + 7.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0, + 12.0, +] + +OUTPUT_DIRECTORY = Path( + "data/processed/lab016" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab016_adaptation_results.csv" +) + +MODE_GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab016_selected_mode.png" +) + +TIME_GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab016_expected_transfer_time.png" +) + + +# ============================================================ +# Выбор исходного кадра +# ============================================================ + +source_candidates = [ + Path( + "data/processed/lab012/" + "03_color_320_q15.jpg" + ), + Path( + "data/raw/lab009_source.jpg" + ), +] + +SOURCE_PATH = next( + ( + path + for path in source_candidates + if path.exists() + ), + None, +) + +if SOURCE_PATH is None: + raise FileNotFoundError( + "Не найден кадр для передачи. " + "Необходимо выполнить Lab009 или Lab012." + ) + +source_bytes = SOURCE_PATH.read_bytes() + +if not source_bytes: + raise ValueError( + "Исходный JPEG пуст" + ) + + +# ============================================================ +# Оценка передачи всего изображения +# ============================================================ + +def estimate_image_transfer( + image_bytes: bytes, + fragment_size: int, + ber: float, + image_id: int, +) -> dict: + """ + Оценить передачу полного изображения с бесконечным ARQ. + + Расчёт учитывает фактический размер последнего фрагмента. + """ + + fragments = split_image_bytes( + image_bytes=image_bytes, + image_id=image_id, + fragment_data_size=fragment_size, + ) + + ack_packet = build_packet( + payload=b"", + message_type=MESSAGE_TYPE_ACK, + sequence_number=0, + ) + + ack_size_bytes = len( + ack_packet + ) + + ack_success_probability = ( + packet_success_probability( + ber=ber, + packet_bit_count=( + ack_size_bytes * 8 + ), + ) + ) + + expected_total_bytes = 0.0 + + for fragment in fragments: + + fragment_payload = encode_image_fragment( + fragment + ) + + data_packet = build_packet( + payload=fragment_payload, + message_type=( + MESSAGE_TYPE_IMAGE_FRAGMENT + ), + sequence_number=( + fragment.fragment_index + ), + ) + + data_success_probability = ( + packet_success_probability( + ber=ber, + packet_bit_count=( + len(data_packet) * 8 + ), + ) + ) + + confirmed_probability = ( + data_success_probability + * ack_success_probability + ) + + expected_total_bytes += ( + len(data_packet) + / confirmed_probability + ) + + expected_total_bytes += ( + ack_size_bytes + / ack_success_probability + ) + + expected_seconds = ( + expected_total_bytes + * 8 + / CHANNEL_BITRATE_BPS + ) + + effective_goodput_bps = ( + len(image_bytes) + * 8 + / expected_seconds + ) + + return { + "fragment_count": len(fragments), + "expected_total_bytes": expected_total_bytes, + "expected_seconds": expected_seconds, + "effective_goodput_bps": ( + effective_goodput_bps + ), + } + + +# ============================================================ +# Адаптация по профилю канала +# ============================================================ + +results = [] + +for step_index, eb_n0_db in enumerate( + EB_N0_PROFILE_DB +): + + decision = choose_image_mode( + eb_n0_db=eb_n0_db, + candidate_fragment_sizes=( + CANDIDATE_FRAGMENT_SIZES + ), + channel_bitrate_bps=( + CHANNEL_BITRATE_BPS + ), + max_attempts=MAX_ATTEMPTS, + minimum_success_probability=0.85, + minimum_goodput_bps=2_000.0, + ) + + if decision.images_enabled: + + selected_estimate = next( + estimate + for estimate in decision.estimates + if ( + estimate.fragment_size + == decision.selected_fragment_size + ) + ) + + image_result = estimate_image_transfer( + image_bytes=source_bytes, + fragment_size=( + decision.selected_fragment_size + ), + ber=selected_estimate.ber, + image_id=2026071700 + step_index, + ) + + fragment_size = ( + decision.selected_fragment_size + ) + + fragment_count = ( + image_result["fragment_count"] + ) + + expected_seconds = ( + image_result["expected_seconds"] + ) + + effective_goodput_bps = ( + image_result[ + "effective_goodput_bps" + ] + ) + + success_with_retries = ( + selected_estimate + .success_probability_with_retries + ) + + expected_attempts = ( + selected_estimate.expected_attempts + ) + + mode_name = ( + f"{fragment_size} B" + ) + + else: + fragment_size = 0 + fragment_count = 0 + expected_seconds = None + effective_goodput_bps = 0.0 + success_with_retries = 0.0 + expected_attempts = 0.0 + mode_name = "IMAGE OFF" + + results.append( + { + "step": step_index, + "eb_n0_db": eb_n0_db, + "mode": mode_name, + "fragment_size": fragment_size, + "fragment_count": fragment_count, + "expected_seconds": expected_seconds, + "effective_goodput_bps": ( + effective_goodput_bps + ), + "success_with_retries": ( + success_with_retries + ), + "expected_attempts": ( + expected_attempts + ), + "reason": decision.reason, + } + ) + + +# ============================================================ +# Вывод +# ============================================================ + +print( + "=== Lab016. Адаптация размера фрагмента ===" +) + +print("\nИсходный кадр:") + +print(SOURCE_PATH) + +print("\nРазмер JPEG:") + +print( + len(source_bytes), + "байт", +) + +print("\nРезультаты адаптации:") + +print( + f"{'Шаг':>5}" + f"{'Eb/N0':>10}" + f"{'Режим':>14}" + f"{'Фрагм.':>9}" + f"{'Попыток':>11}" + f"{'Успех x5':>12}" + f"{'Время кадра':>15}" + f"{'Goodput':>13}" +) + +print("-" * 89) + +for result in results: + + if result["expected_seconds"] is None: + time_text = "—" + else: + time_text = ( + f"{result['expected_seconds']:.2f} с" + ) + + print( + f"{result['step']:>5}" + f"{result['eb_n0_db']:>7.1f} дБ" + f"{result['mode']:>14}" + f"{result['fragment_count']:>9}" + f"{result['expected_attempts']:>11.2f}" + f"{result['success_with_retries'] * 100:>10.1f} %" + f"{time_text:>15}" + f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с" + ) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + fieldnames = list( + results[0].keys() + ) + + writer = DictWriter( + csv_file, + fieldnames=fieldnames, + ) + + writer.writeheader() + writer.writerows(results) + + +# ============================================================ +# График выбранного режима +# ============================================================ + +steps = [ + result["step"] + for result in results +] + +fragment_sizes = [ + result["fragment_size"] + for result in results +] + +plt.figure( + figsize=(11, 6) +) + +plt.step( + steps, + fragment_sizes, + where="mid", + marker="o", +) + +plt.yticks( + [ + 0, + 128, + 512, + 1024, + ], + [ + "IMAGE OFF", + "128 B", + "512 B", + "1024 B", + ], +) + +plt.xlabel( + "Шаг времени" +) + +plt.ylabel( + "Выбранный режим" +) + +plt.title( + "Автоматический выбор размера фрагмента" +) + +plt.grid( + True +) + +plt.tight_layout() + +plt.savefig( + MODE_GRAPH_PATH, + dpi=160, +) + +plt.close() + + +# ============================================================ +# График времени передачи кадра +# ============================================================ + +transfer_times = np.array( + [ + ( + result["expected_seconds"] + if result["expected_seconds"] is not None + else np.nan + ) + for result in results + ], + dtype=np.float64, +) + +plt.figure( + figsize=(11, 6) +) + +plt.plot( + steps, + transfer_times, + marker="o", +) + +plt.xlabel( + "Шаг времени" +) + +plt.ylabel( + "Ожидаемое время передачи кадра, с" +) + +plt.title( + "Время передачи кадра при адаптации канала" +) + +plt.grid( + True +) + +plt.tight_layout() + +plt.savefig( + TIME_GRAPH_PATH, + dpi=160, +) + +plt.close() + + +# ============================================================ +# Проверки ожидаемой логики +# ============================================================ + +decision_at_6_db = next( + result + for result in results + if result["eb_n0_db"] == 6.0 +) + +decision_at_8_db = next( + result + for result in results + if result["eb_n0_db"] == 8.0 +) + +decision_at_9_db = next( + result + for result in results + if result["eb_n0_db"] == 9.0 +) + +decision_at_10_db = next( + result + for result in results + if result["eb_n0_db"] == 10.0 +) + +assert ( + decision_at_6_db["fragment_size"] + == 0 +) + +assert ( + decision_at_8_db["fragment_size"] + == 128 +) + +assert ( + decision_at_9_db["fragment_size"] + == 512 +) + +assert ( + decision_at_10_db["fragment_size"] + == 1024 +) + +assert CSV_PATH.exists() +assert MODE_GRAPH_PATH.exists() +assert TIME_GRAPH_PATH.exists() + + +print("\nCSV:") + +print(CSV_PATH) + +print("\nГрафик режимов:") + +print(MODE_GRAPH_PATH) + +print("\nГрафик времени:") + +print(TIME_GRAPH_PATH) + +print( + "\nПроверка пройдена: " + "адаптивный выбор режима работает." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab017_link_hysteresis.py b/Projects/SDR_Rover/tests/lab017_link_hysteresis.py new file mode 100644 index 0000000..7198082 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab017_link_hysteresis.py @@ -0,0 +1,468 @@ +""" +Lab017. Фильтрация Eb/N0 и гистерезис режимов. + +Эксперимент сравнивает два адаптера: + +1. Наивный адаптер: + переключает режим при каждом пересечении порога. + +2. Устойчивый адаптер: + использует: + - экспоненциальное усреднение; + - гистерезис; + - подтверждение повышения; + - аварийное быстрое понижение. + +Профиль Eb/N0 специально содержит колебания +около порогов переключения. +""" + +from csv import DictWriter +from pathlib import Path + +import matplotlib.pyplot as plt + +from protocol.link_controller import ( + FRAGMENT_MODE_1024, + IMAGE_OFF, + LinkModeController, +) + + +# ============================================================ +# Настройки +# ============================================================ + +OUTPUT_DIRECTORY = Path( + "data/processed/lab017" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab017_hysteresis_results.csv" +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab017_hysteresis_comparison.png" +) + + +# ============================================================ +# Профиль качества канала +# ============================================================ + +EB_N0_PROFILE_DB = [ + 10.2, + 9.7, + 9.4, + 9.6, + 9.3, + 9.7, + 9.2, + 9.6, + 9.0, + 8.7, + 8.4, + 8.6, + 8.3, + 8.7, + 8.2, + 8.6, + 8.0, + 7.8, + 7.5, + 7.7, + 7.4, + 7.8, + 7.2, + 6.8, + 6.5, + 6.3, + 6.6, + 6.2, + 6.7, + 6.1, + 5.5, + 6.0, + 6.4, + 6.7, + 6.9, + 7.1, + 7.3, + 8.0, + 8.8, + 9.0, + 9.4, + 9.8, + 10.0, + 10.2, + 10.1, +] + + +# ============================================================ +# Наивное решение без гистерезиса +# ============================================================ + +def choose_naive_mode( + eb_n0_db: float, +) -> int: + """ + Немедленно выбрать режим по одному измерению. + + Гистерезиса и фильтрации нет. + """ + + if eb_n0_db < 6.5: + return 0 + + if eb_n0_db < 8.5: + return 128 + + if eb_n0_db < 9.5: + return 512 + + return 1024 + + +# ============================================================ +# Запуск устойчивого контроллера +# ============================================================ + +controller = LinkModeController( + initial_mode=FRAGMENT_MODE_1024, + filter_alpha=0.35, + upgrade_confirmation_count=3, + minimum_hold_steps=2, +) + +results = [] + +for measured_eb_n0_db in EB_N0_PROFILE_DB: + + stable_result = controller.update( + measured_eb_n0_db + ) + + naive_mode = choose_naive_mode( + measured_eb_n0_db + ) + + results.append( + { + "step": stable_result.step, + "measured_eb_n0_db": ( + stable_result.measured_eb_n0_db + ), + "filtered_eb_n0_db": ( + stable_result.filtered_eb_n0_db + ), + "naive_mode": naive_mode, + "stable_mode": ( + stable_result.selected_mode + ), + "mode_changed": ( + stable_result.mode_changed + ), + "pending_upgrade_mode": ( + stable_result.pending_upgrade_mode + ), + "pending_upgrade_count": ( + stable_result.pending_upgrade_count + ), + "reason": stable_result.reason, + } + ) + + +# ============================================================ +# Число переключений +# ============================================================ + +def count_mode_switches( + modes: list[int], +) -> int: + """ + Посчитать число изменений режима. + """ + + return sum( + previous_mode != current_mode + for previous_mode, current_mode in zip( + modes, + modes[1:], + ) + ) + + +naive_modes = [ + result["naive_mode"] + for result in results +] + +stable_modes = [ + result["stable_mode"] + for result in results +] + +naive_switch_count = count_mode_switches( + naive_modes +) + +stable_switch_count = count_mode_switches( + stable_modes +) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print( + "=== Lab017. Гистерезис режима канала ===" +) + +print("\nРезультаты по шагам:") + +print( + f"{'Шаг':>5}" + f"{'Eb/N0':>10}" + f"{'Фильтр':>11}" + f"{'Наивный':>11}" + f"{'Устойчивый':>13}" + f"{'Изменён':>10}" + f"{'Причина':>45}" +) + +print("-" * 105) + +for result in results: + + print( + f"{result['step']:>5}" + f"{result['measured_eb_n0_db']:>7.1f} дБ" + f"{result['filtered_eb_n0_db']:>8.2f} дБ" + f"{result['naive_mode']:>11}" + f"{result['stable_mode']:>13}" + f"{str(result['mode_changed']):>10}" + f" {result['reason']}" + ) + + +print("\nЧисло переключений:") + +print( + "Наивный адаптер:", + naive_switch_count, +) + +print( + "Устойчивый адаптер:", + stable_switch_count, +) + +print( + "Предотвращено лишних переключений:", + naive_switch_count - stable_switch_count, +) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + writer = DictWriter( + csv_file, + fieldnames=list( + results[0].keys() + ), + ) + + writer.writeheader() + writer.writerows(results) + + +# ============================================================ +# График +# ============================================================ + +steps = [ + result["step"] + for result in results +] + +measured_values = [ + result["measured_eb_n0_db"] + for result in results +] + +filtered_values = [ + result["filtered_eb_n0_db"] + for result in results +] + + +figure, axes = plt.subplots( + 2, + 1, + figsize=(12, 9), + sharex=True, +) + + +# ------------------------------------------------------------ +# Верхний график: Eb/N0 +# ------------------------------------------------------------ + +axes[0].plot( + steps, + measured_values, + marker=".", + label="Измеренное Eb/N0", +) + +axes[0].plot( + steps, + filtered_values, + linewidth=2, + label="Фильтрованное Eb/N0", +) + +axes[0].axhline( + 6.5, + linestyle=":", + label="Порог наивного адаптера", +) + +axes[0].axhline( + 8.5, + linestyle=":", +) + +axes[0].axhline( + 9.5, + linestyle=":", +) + +axes[0].set_ylabel( + "Eb/N0, дБ" +) + +axes[0].set_title( + "Фильтрация измерения качества канала" +) + +axes[0].grid( + True +) + +axes[0].legend() + + +# ------------------------------------------------------------ +# Нижний график: режимы +# ------------------------------------------------------------ + +axes[1].step( + steps, + naive_modes, + where="mid", + marker=".", + label="Наивный адаптер", +) + +axes[1].step( + steps, + stable_modes, + where="mid", + marker="o", + label="Гистерезис и фильтрация", +) + +axes[1].set_yticks( + [ + 0, + 128, + 512, + 1024, + ], + [ + "IMAGE OFF", + "128 B", + "512 B", + "1024 B", + ], +) + +axes[1].set_xlabel( + "Шаг времени" +) + +axes[1].set_ylabel( + "Режим" +) + +axes[1].set_title( + "Сравнение переключений режимов" +) + +axes[1].grid( + True +) + +axes[1].legend() + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Проверки +# ============================================================ + +assert results + +assert IMAGE_OFF in stable_modes + +assert FRAGMENT_MODE_1024 in stable_modes + +assert stable_switch_count < naive_switch_count + +assert CSV_PATH.exists() + +assert GRAPH_PATH.exists() + + +print("\nCSV:") + +print(CSV_PATH) + +print("\nГрафик:") + +print(GRAPH_PATH) + +print( + "\nПроверка пройдена: " + "гистерезис уменьшил количество переключений." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab018_bpsk_radio_frame.py b/Projects/SDR_Rover/tests/lab018_bpsk_radio_frame.py new file mode 100644 index 0000000..263b199 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab018_bpsk_radio_frame.py @@ -0,0 +1,1148 @@ +""" +Lab018. Формирование BPSK-радиокадра. + +Программа: + +1. Формирует внутренний пакет SDR Rover Link. +2. Добавляет радиопреамбулу. +3. Добавляет внешнее синхрослово. +4. Добавляет длину внутреннего пакета. +5. Преобразует радиокадр в биты. +6. Модулирует биты методом BPSK. +7. Увеличивает частоту дискретизации. +8. Применяет Root Raised Cosine-фильтр. +9. Формирует комплексные IQ-сэмплы. +10. Сохраняет IQ в форматах NPY и CF32. +11. Строит графики кадра, сигнала и спектра. + +Передача через SDR в этой лабораторной не выполняется. +""" + +from pathlib import Path +import struct + +import matplotlib.pyplot as plt +import numpy as np + +from protocol.packet import ( + MESSAGE_TYPE_TEXT, + build_packet, +) + + +# ============================================================ +# Настройки содержимого +# ============================================================ + +MESSAGE = "ПРИВЕТ SDR" + +SEQUENCE_NUMBER = 18 + + +# ============================================================ +# Настройки радиокадра +# ============================================================ + +# Радиосинхрослово физического уровня. +# +# Не следует путать с SYNC_WORD = 0xAA55, +# который находится внутри packet.py. +RADIO_SYNC_WORD = 0xD391 + +# Преамбула 10101010... +PREAMBLE_BIT_COUNT = 64 + +# Пустой участок до и после радиокадра. +GUARD_SYMBOL_COUNT = 16 + + +# ============================================================ +# Настройки BPSK +# ============================================================ + +# BPSK передаёт один бит одним символом. +SYMBOL_RATE = 20_000 + +# На один BPSK-символ приходится 32 IQ-сэмпла. +SAMPLES_PER_SYMBOL = 32 + +SAMPLE_RATE = ( + SYMBOL_RATE + * SAMPLES_PER_SYMBOL +) + +# Коэффициент скругления RRC-фильтра. +RRC_ROLLOFF = 0.35 + +# Длина фильтра в символах. +# +# Используем чётное значение. +RRC_SPAN_SYMBOLS = 10 + +# Амплитудный запас. +# +# Не используем полную шкалу ±1, +# чтобы позднее не перегружать передатчик. +TX_AMPLITUDE = 0.70 + + +# ============================================================ +# Выходные файлы +# ============================================================ + +OUTPUT_DIRECTORY = Path( + "data/processed/lab018" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +NPY_PATH = ( + OUTPUT_DIRECTORY + / "lab018_bpsk_tx_iq.npy" +) + +CF32_PATH = ( + OUTPUT_DIRECTORY + / "lab018_bpsk_tx_iq.cf32" +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab018_bpsk_radio_frame.png" +) + +INFO_PATH = ( + OUTPUT_DIRECTORY + / "lab018_frame_info.txt" +) + + +# ============================================================ +# Преобразование байтов в биты +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать bytes в массив битов 0 и 1. + + Используется порядок от старшего бита к младшему. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + byte_array = np.frombuffer( + data, + dtype=np.uint8, + ) + + return np.unpackbits( + byte_array + ) + + +# ============================================================ +# BPSK-модулятор +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в BPSK-символы. + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + symbols = ( + 2.0 * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +# ============================================================ +# Root Raised Cosine-фильтр +# ============================================================ + +def root_raised_cosine_taps( + rolloff: float, + samples_per_symbol: int, + span_symbols: int, +) -> np.ndarray: + """ + Рассчитать коэффициенты Root Raised Cosine-фильтра. + + Параметры + ---------- + rolloff: + Коэффициент скругления beta. + + samples_per_symbol: + Количество сэмплов на символ. + + span_symbols: + Полная длина фильтра в символах. + + Возвращает + ---------- + Одномерный массив коэффициентов фильтра. + """ + + if not 0.0 < rolloff <= 1.0: + raise ValueError( + "rolloff должен находиться в диапазоне 0...1" + ) + + if samples_per_symbol <= 0: + raise ValueError( + "samples_per_symbol должен быть положительным" + ) + + if span_symbols <= 0: + raise ValueError( + "span_symbols должен быть положительным" + ) + + if span_symbols % 2 != 0: + raise ValueError( + "span_symbols должен быть чётным" + ) + + half_sample_count = ( + span_symbols + * samples_per_symbol + // 2 + ) + + sample_indexes = np.arange( + -half_sample_count, + half_sample_count + 1, + dtype=np.float64, + ) + + # Время нормировано к длительности одного символа. + time_values = ( + sample_indexes + / samples_per_symbol + ) + + taps = np.zeros_like( + time_values + ) + + beta = rolloff + + for index, time_value in enumerate( + time_values + ): + + # Особая точка t = 0. + if np.isclose( + time_value, + 0.0, + ): + taps[index] = ( + 1.0 + - beta + + ( + 4.0 + * beta + / np.pi + ) + ) + + continue + + # Особые точки t = ±1/(4 beta). + if np.isclose( + abs(time_value), + 1.0 / (4.0 * beta), + ): + taps[index] = ( + beta + / np.sqrt(2.0) + * ( + ( + 1.0 + + 2.0 / np.pi + ) + * np.sin( + np.pi + / (4.0 * beta) + ) + + ( + 1.0 + - 2.0 / np.pi + ) + * np.cos( + np.pi + / (4.0 * beta) + ) + ) + ) + + continue + + numerator = ( + np.sin( + np.pi + * time_value + * (1.0 - beta) + ) + + ( + 4.0 + * beta + * time_value + * np.cos( + np.pi + * time_value + * (1.0 + beta) + ) + ) + ) + + denominator = ( + np.pi + * time_value + * ( + 1.0 + - ( + 4.0 + * beta + * time_value + ) ** 2 + ) + ) + + taps[index] = ( + numerator + / denominator + ) + + # Нормируем энергию фильтра. + taps /= np.sqrt( + np.sum( + taps ** 2 + ) + ) + + return taps + + +# ============================================================ +# Увеличение частоты дискретизации +# ============================================================ + +def upsample_symbols( + symbols: np.ndarray, + samples_per_symbol: int, +) -> np.ndarray: + """ + Вставить между символами нулевые сэмплы. + + Пример при SPS = 4: + + [1, -1] + ↓ + [1, 0, 0, 0, -1, 0, 0, 0] + """ + + symbols = np.asarray( + symbols, + dtype=np.complex128, + ) + + upsampled = np.zeros( + len(symbols) + * samples_per_symbol, + dtype=np.complex128, + ) + + upsampled[ + ::samples_per_symbol + ] = symbols + + return upsampled + + +# ============================================================ +# Спектр +# ============================================================ + +def calculate_normalized_spectrum( + iq_samples: np.ndarray, + sample_rate: float, +) -> tuple[np.ndarray, np.ndarray]: + """ + Рассчитать нормированный амплитудный спектр. + """ + + fft_size = 2 ** int( + np.ceil( + np.log2( + max( + len(iq_samples), + 1024, + ) + ) + ) + ) + + spectrum = np.fft.fftshift( + np.fft.fft( + iq_samples, + n=fft_size, + ) + ) + + frequencies = np.fft.fftshift( + np.fft.fftfreq( + fft_size, + d=1.0 / sample_rate, + ) + ) + + magnitude = np.abs( + spectrum + ) + + maximum_magnitude = np.max( + magnitude + ) + + magnitude_db = 20.0 * np.log10( + magnitude / maximum_magnitude + + 1e-12 + ) + + return frequencies, magnitude_db + + +# ============================================================ +# Формирование внутреннего пакета +# ============================================================ + +payload = MESSAGE.encode( + "utf-8" +) + +protocol_packet = build_packet( + payload=payload, + message_type=MESSAGE_TYPE_TEXT, + sequence_number=SEQUENCE_NUMBER, +) + + +# ============================================================ +# Формирование внешнего заголовка радиокадра +# ============================================================ + +if len(protocol_packet) > 65535: + raise ValueError( + "Внутренний пакет слишком велик " + "для 16-битного поля длины" + ) + +radio_header = struct.pack( + ">HH", + RADIO_SYNC_WORD, + len(protocol_packet), +) + +radio_header_bits = bytes_to_bits( + radio_header +) + +protocol_packet_bits = bytes_to_bits( + protocol_packet +) + +# Преамбула 101010... +preamble_bits = np.tile( + np.array( + [1, 0], + dtype=np.uint8, + ), + PREAMBLE_BIT_COUNT // 2, +) + +radio_frame_bits = np.concatenate( + [ + preamble_bits, + radio_header_bits, + protocol_packet_bits, + ] +) + + +# ============================================================ +# BPSK-модуляция +# ============================================================ + +radio_frame_symbols = bpsk_modulate( + radio_frame_bits +) + +upsampled_symbols = upsample_symbols( + radio_frame_symbols, + SAMPLES_PER_SYMBOL, +) + + +# ============================================================ +# Guard interval +# ============================================================ + +guard_samples = np.zeros( + GUARD_SYMBOL_COUNT + * SAMPLES_PER_SYMBOL, + dtype=np.complex128, +) + +guarded_upsampled_symbols = np.concatenate( + [ + guard_samples, + upsampled_symbols, + guard_samples, + ] +) + + +# ============================================================ +# RRC-фильтрация +# ============================================================ + +rrc_taps = root_raised_cosine_taps( + rolloff=RRC_ROLLOFF, + samples_per_symbol=SAMPLES_PER_SYMBOL, + span_symbols=RRC_SPAN_SYMBOLS, +) + +filtered_iq = np.convolve( + guarded_upsampled_symbols, + rrc_taps, + mode="full", +) + + +# ============================================================ +# Нормирование амплитуды +# ============================================================ + +maximum_amplitude = np.max( + np.abs( + filtered_iq + ) +) + +if maximum_amplitude == 0.0: + raise RuntimeError( + "Сформирован нулевой IQ-сигнал" + ) + +normalized_iq = ( + filtered_iq + / maximum_amplitude + * TX_AMPLITUDE +) + +tx_iq = normalized_iq.astype( + np.complex64 +) + + +# ============================================================ +# Параметры сигнала +# ============================================================ + +frame_duration_seconds = ( + len(tx_iq) + / SAMPLE_RATE +) + +theoretical_baseband_bandwidth_hz = ( + SYMBOL_RATE + * (1.0 + RRC_ROLLOFF) +) + +average_power = float( + np.mean( + np.abs(tx_iq) ** 2 + ) +) + +peak_power = float( + np.max( + np.abs(tx_iq) ** 2 + ) +) + +papr_db = 10.0 * np.log10( + peak_power / average_power +) + + +# ============================================================ +# Сохранение IQ +# ============================================================ + +# Формат NPY сохраняет тип и форму массива. +np.save( + NPY_PATH, + tx_iq, +) + +# complex64 записывается как чередующиеся: +# +# float32 I, float32 Q, float32 I, float32 Q... +tx_iq.tofile( + CF32_PATH +) + + +# ============================================================ +# Проверка сохранённых данных +# ============================================================ + +loaded_npy_iq = np.load( + NPY_PATH +) + +loaded_cf32_iq = np.fromfile( + CF32_PATH, + dtype=np.complex64, +) + +assert np.array_equal( + loaded_npy_iq, + tx_iq, +) + +assert np.array_equal( + loaded_cf32_iq, + tx_iq, +) + + +# ============================================================ +# Спектр +# ============================================================ + +frequencies_hz, spectrum_db = ( + calculate_normalized_spectrum( + iq_samples=tx_iq, + sample_rate=SAMPLE_RATE, + ) +) + + +# ============================================================ +# Графики +# ============================================================ + +figure, axes = plt.subplots( + 4, + 1, + figsize=(13, 13), +) + + +# ------------------------------------------------------------ +# 1. Структура битового радиокадра +# ------------------------------------------------------------ + +bit_indexes = np.arange( + len(radio_frame_bits) +) + +axes[0].step( + bit_indexes, + radio_frame_bits, + where="post", +) + +preamble_end = ( + PREAMBLE_BIT_COUNT +) + +sync_end = ( + preamble_end + 16 +) + +length_end = ( + sync_end + 16 +) + +axes[0].axvline( + preamble_end, + linestyle="--", +) + +axes[0].axvline( + sync_end, + linestyle="--", +) + +axes[0].axvline( + length_end, + linestyle="--", +) + +axes[0].text( + PREAMBLE_BIT_COUNT / 2, + 1.08, + "PREAMBLE", + ha="center", +) + +axes[0].text( + preamble_end + 8, + 1.08, + "SYNC", + ha="center", +) + +axes[0].text( + sync_end + 8, + 1.08, + "LENGTH", + ha="center", +) + +axes[0].text( + length_end + + len(protocol_packet_bits) / 2, + 1.08, + "SDR ROVER PACKET", + ha="center", +) + +axes[0].set_ylim( + -0.2, + 1.3, +) + +axes[0].set_xlabel( + "Номер бита" +) + +axes[0].set_ylabel( + "Бит" +) + +axes[0].set_title( + "Структура радиокадра" +) + +axes[0].grid( + True +) + + +# ------------------------------------------------------------ +# 2. Первые BPSK-символы +# ------------------------------------------------------------ + +symbol_plot_count = min( + 100, + len(radio_frame_symbols), +) + +axes[1].step( + np.arange( + symbol_plot_count + ), + radio_frame_symbols[ + :symbol_plot_count + ].real, + where="post", +) + +axes[1].set_xlabel( + "Номер символа" +) + +axes[1].set_ylabel( + "Амплитуда BPSK" +) + +axes[1].set_title( + "Первые BPSK-символы" +) + +axes[1].set_ylim( + -1.3, + 1.3, +) + +axes[1].grid( + True +) + + +# ------------------------------------------------------------ +# 3. Отфильтрованный IQ-сигнал +# ------------------------------------------------------------ + +filter_delay_samples = ( + len(rrc_taps) - 1 +) // 2 + +frame_start_sample = ( + GUARD_SYMBOL_COUNT + * SAMPLES_PER_SYMBOL + + filter_delay_samples +) + +waveform_plot_symbol_count = 20 + +waveform_plot_start = ( + frame_start_sample +) + +waveform_plot_end = ( + waveform_plot_start + + waveform_plot_symbol_count + * SAMPLES_PER_SYMBOL +) + +waveform_plot_samples = tx_iq[ + waveform_plot_start: + waveform_plot_end +] + +time_axis_ms = ( + np.arange( + len(waveform_plot_samples) + ) + / SAMPLE_RATE + * 1000.0 +) + +axes[2].plot( + time_axis_ms, + waveform_plot_samples.real, + label="I", +) + +axes[2].plot( + time_axis_ms, + waveform_plot_samples.imag, + label="Q", +) + +axes[2].set_xlabel( + "Время, мс" +) + +axes[2].set_ylabel( + "Амплитуда" +) + +axes[2].set_title( + "BPSK после Root Raised Cosine-фильтра" +) + +axes[2].grid( + True +) + +axes[2].legend() + + +# ------------------------------------------------------------ +# 4. Спектр +# ------------------------------------------------------------ + +axes[3].plot( + frequencies_hz / 1000.0, + spectrum_db, +) + +axes[3].axvline( + theoretical_baseband_bandwidth_hz + / 2.0 + / 1000.0, + linestyle="--", +) + +axes[3].axvline( + -theoretical_baseband_bandwidth_hz + / 2.0 + / 1000.0, + linestyle="--", +) + +axes[3].set_xlim( + -100, + 100, +) + +axes[3].set_ylim( + -100, + 5, +) + +axes[3].set_xlabel( + "Частота относительно несущей, кГц" +) + +axes[3].set_ylabel( + "Нормированный уровень, дБ" +) + +axes[3].set_title( + "Спектр сформированного IQ-сигнала" +) + +axes[3].grid( + True +) + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Текстовый отчёт +# ============================================================ + +report_lines = [ + "Lab018. BPSK radio frame", + "", + f"Message: {MESSAGE}", + f"Protocol packet bytes: {len(protocol_packet)}", + f"Preamble bits: {len(preamble_bits)}", + f"Radio header bits: {len(radio_header_bits)}", + f"Protocol packet bits: {len(protocol_packet_bits)}", + f"Total radio frame bits: {len(radio_frame_bits)}", + f"Symbol rate: {SYMBOL_RATE} symbols/s", + f"Samples per symbol: {SAMPLES_PER_SYMBOL}", + f"Sample rate: {SAMPLE_RATE} samples/s", + f"RRC rolloff: {RRC_ROLLOFF}", + f"RRC taps: {len(rrc_taps)}", + f"IQ sample count: {len(tx_iq)}", + f"Frame duration: {frame_duration_seconds:.6f} s", + ( + "Approximate occupied bandwidth: " + f"{theoretical_baseband_bandwidth_hz:.1f} Hz" + ), + f"Average normalized power: {average_power:.6f}", + f"Peak normalized power: {peak_power:.6f}", + f"PAPR: {papr_db:.2f} dB", + f"NPY file: {NPY_PATH}", + f"CF32 file: {CF32_PATH}", +] + +INFO_PATH.write_text( + "\n".join( + report_lines + ), + encoding="utf-8", +) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print( + "=== Lab018. Формирование BPSK-радиокадра ===" +) + +print("\nИсходное сообщение:") + +print(MESSAGE) + +print("\nВнутренний пакет SDR Rover Link:") + +print( + len(protocol_packet), + "байт", +) + +print("\nРадиосинхрослово:") + +print( + f"0x{RADIO_SYNC_WORD:04X}" +) + +print("\nСтруктура радиокадра:") + +print( + "Преамбула:", + len(preamble_bits), + "бит", +) + +print( + "Радиозаголовок:", + len(radio_header_bits), + "бит", +) + +print( + "Внутренний пакет:", + len(protocol_packet_bits), + "бит", +) + +print( + "Всего:", + len(radio_frame_bits), + "битов и BPSK-символов", +) + +print("\nПараметры дискретизации:") + +print( + "Символьная скорость:", + SYMBOL_RATE, + "символов/с", +) + +print( + "Сэмплов на символ:", + SAMPLES_PER_SYMBOL, +) + +print( + "Частота дискретизации:", + SAMPLE_RATE, + "сэмплов/с", +) + +print("\nRRC-фильтр:") + +print( + "Rolloff:", + RRC_ROLLOFF, +) + +print( + "Количество коэффициентов:", + len(rrc_taps), +) + +print("\nIQ-сигнал:") + +print( + "Количество сэмплов:", + len(tx_iq), +) + +print( + "Длительность:", + f"{frame_duration_seconds * 1000:.2f}", + "мс", +) + +print( + "Максимальная амплитуда:", + f"{np.max(np.abs(tx_iq)):.3f}", +) + +print( + "PAPR:", + f"{papr_db:.2f}", + "дБ", +) + +print("\nОценочная ширина спектра:") + +print( + f"{theoretical_baseband_bandwidth_hz / 1000:.1f}", + "кГц", +) + +print("\nФайл NumPy:") + +print(NPY_PATH) + +print("\nФайл raw complex float32:") + +print(CF32_PATH) + +print("\nГрафик:") + +print(GRAPH_PATH) + +print("\nОтчёт:") + +print(INFO_PATH) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert len(preamble_bits) == PREAMBLE_BIT_COUNT + +assert radio_header[:2] == struct.pack( + ">H", + RADIO_SYNC_WORD, +) + +assert len(radio_header) == 4 + +assert len(radio_frame_symbols) == len( + radio_frame_bits +) + +assert np.iscomplexobj( + tx_iq +) + +assert tx_iq.dtype == np.complex64 + +assert np.max( + np.abs(tx_iq) +) <= TX_AMPLITUDE + 1e-6 + +assert NPY_PATH.exists() +assert CF32_PATH.exists() +assert GRAPH_PATH.exists() +assert INFO_PATH.exists() + +print( + "\nПроверка пройдена: " + "реальный BPSK-радиокадр и IQ-сэмплы сформированы." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab019_bpsk_receiver.py b/Projects/SDR_Rover/tests/lab019_bpsk_receiver.py new file mode 100644 index 0000000..29f5ec6 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab019_bpsk_receiver.py @@ -0,0 +1,1212 @@ +""" +Lab019. Программный приёмник BPSK-радиокадра. + +Программа: + +1. Загружает IQ-сэмплы, созданные в Lab018. +2. Намеренно добавляет неизвестную задержку. +3. Намеренно поворачивает фазу сигнала. +4. Выполняет согласованную RRC-фильтрацию. +5. Перебирает все возможные фазы дискретизации. +6. Ищет PREAMBLE + RADIO SYNC корреляционным методом. +7. Оценивает фазовый поворот BPSK. +8. Восстанавливает биты радиокадра. +9. Читает длину внутреннего пакета. +10. Разбирает пакет SDR Rover Link. +11. Проверяет CRC-32. +12. Восстанавливает исходное сообщение. + +Упрощения: + +- частота передатчика и приёмника совпадает; +- частота дискретизации совпадает; +- доплеровский сдвиг отсутствует; +- многолучёвость отсутствует; +- шум пока не добавляется. +""" + +from pathlib import Path +import struct + +import matplotlib.pyplot as plt +import numpy as np + +from protocol.packet import ( + MESSAGE_TYPE_TEXT, + parse_packet, +) + + +# ============================================================ +# Настройки радиокадра +# ============================================================ + +RADIO_SYNC_WORD = 0xD391 + +PREAMBLE_BIT_COUNT = 64 + +SAMPLES_PER_SYMBOL = 32 + +RRC_ROLLOFF = 0.35 +RRC_SPAN_SYMBOLS = 10 + + +# ============================================================ +# Контрольные данные +# ============================================================ + +EXPECTED_MESSAGE = "ПРИВЕТ SDR" +EXPECTED_SEQUENCE_NUMBER = 18 + + +# ============================================================ +# Искусственные искажения для проверки приёмника +# ============================================================ + +# Приёмнику заранее неизвестно, +# с какого конкретно сэмпла начинается сигнал. +TEST_SAMPLE_DELAY = 11 + +# Приёмнику также заранее неизвестна +# фаза несущей BPSK. +TEST_PHASE_OFFSET_DEGREES = 37.0 + + +# ============================================================ +# Пути +# ============================================================ + +INPUT_IQ_PATH = Path( + "data/processed/lab018/" + "lab018_bpsk_tx_iq.npy" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab019" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab019_bpsk_receiver.png" +) + +REPORT_PATH = ( + OUTPUT_DIRECTORY + / "lab019_receiver_report.txt" +) + +RECOVERED_PACKET_PATH = ( + OUTPUT_DIRECTORY + / "lab019_recovered_packet.bin" +) + + +# ============================================================ +# Преобразование bytes → bits +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать bytes в массив битов 0 и 1. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + byte_array = np.frombuffer( + data, + dtype=np.uint8, + ) + + return np.unpackbits( + byte_array + ) + + +# ============================================================ +# Преобразование bits → bytes +# ============================================================ + +def bits_to_bytes( + bits: np.ndarray, +) -> bytes: + """ + Упаковать отдельные биты обратно в bytes. + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if len(bits) % 8 != 0: + raise ValueError( + "Количество битов должно быть кратно восьми" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + return np.packbits( + bits + ).tobytes() + + +# ============================================================ +# BPSK-модулятор для опорной последовательности +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в BPSK-символы. + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + symbols = ( + 2.0 * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +# ============================================================ +# BPSK-демодулятор +# ============================================================ + +def bpsk_demodulate( + symbols: np.ndarray, +) -> np.ndarray: + """ + Принять решение по знаку компоненты I. + + I < 0 → 0 + I >= 0 → 1 + """ + + return ( + symbols.real >= 0.0 + ).astype(np.uint8) + + +# ============================================================ +# Root Raised Cosine-фильтр +# ============================================================ + +def root_raised_cosine_taps( + rolloff: float, + samples_per_symbol: int, + span_symbols: int, +) -> np.ndarray: + """ + Рассчитать коэффициенты RRC-фильтра. + + Формула и параметры совпадают с Lab018. + """ + + if not 0.0 < rolloff <= 1.0: + raise ValueError( + "rolloff должен находиться в диапазоне 0...1" + ) + + if samples_per_symbol <= 0: + raise ValueError( + "samples_per_symbol должен быть положительным" + ) + + if span_symbols <= 0: + raise ValueError( + "span_symbols должен быть положительным" + ) + + if span_symbols % 2 != 0: + raise ValueError( + "span_symbols должен быть чётным" + ) + + half_sample_count = ( + span_symbols + * samples_per_symbol + // 2 + ) + + sample_indexes = np.arange( + -half_sample_count, + half_sample_count + 1, + dtype=np.float64, + ) + + time_values = ( + sample_indexes + / samples_per_symbol + ) + + taps = np.zeros_like( + time_values + ) + + beta = rolloff + + for index, time_value in enumerate( + time_values + ): + + if np.isclose( + time_value, + 0.0, + ): + taps[index] = ( + 1.0 + - beta + + 4.0 * beta / np.pi + ) + + continue + + if np.isclose( + abs(time_value), + 1.0 / (4.0 * beta), + ): + taps[index] = ( + beta + / np.sqrt(2.0) + * ( + ( + 1.0 + + 2.0 / np.pi + ) + * np.sin( + np.pi + / (4.0 * beta) + ) + + ( + 1.0 + - 2.0 / np.pi + ) + * np.cos( + np.pi + / (4.0 * beta) + ) + ) + ) + + continue + + numerator = ( + np.sin( + np.pi + * time_value + * (1.0 - beta) + ) + + ( + 4.0 + * beta + * time_value + * np.cos( + np.pi + * time_value + * (1.0 + beta) + ) + ) + ) + + denominator = ( + np.pi + * time_value + * ( + 1.0 + - ( + 4.0 + * beta + * time_value + ) ** 2 + ) + ) + + taps[index] = ( + numerator + / denominator + ) + + taps /= np.sqrt( + np.sum( + taps ** 2 + ) + ) + + return taps + + +# ============================================================ +# Формирование известного маркера +# ============================================================ + +def build_frame_marker( +) -> tuple[np.ndarray, np.ndarray]: + """ + Сформировать: + + PREAMBLE + RADIO SYNC + + Возвращает биты и BPSK-символы маркера. + """ + + preamble_bits = np.tile( + np.array( + [1, 0], + dtype=np.uint8, + ), + PREAMBLE_BIT_COUNT // 2, + ) + + sync_bytes = struct.pack( + ">H", + RADIO_SYNC_WORD, + ) + + sync_bits = bytes_to_bits( + sync_bytes + ) + + marker_bits = np.concatenate( + [ + preamble_bits, + sync_bits, + ] + ) + + marker_symbols = bpsk_modulate( + marker_bits + ) + + return marker_bits, marker_symbols + + +# ============================================================ +# Корреляционный поиск радиокадра +# ============================================================ + +def find_radio_frame( + matched_iq: np.ndarray, + marker_symbols: np.ndarray, + samples_per_symbol: int, +) -> dict: + """ + Найти фазу дискретизации и начало радиокадра. + + Для каждой возможной фазы: + + 0, 1, 2, ... SPS - 1 + + берём по одному сэмплу на символ и вычисляем + нормированную корреляцию с известным маркером. + + Использование комплексной корреляции позволяет + одновременно оценить постоянный фазовый поворот. + """ + + marker_energy = float( + np.sum( + np.abs(marker_symbols) ** 2 + ) + ) + + best_result = None + + for sample_phase in range( + samples_per_symbol + ): + + symbol_samples = matched_iq[ + sample_phase::samples_per_symbol + ] + + if len(symbol_samples) < len( + marker_symbols + ): + continue + + correlation = np.correlate( + symbol_samples, + marker_symbols, + mode="valid", + ) + + window_energy = np.convolve( + np.abs(symbol_samples) ** 2, + np.ones( + len(marker_symbols) + ), + mode="valid", + ) + + denominator = ( + np.sqrt( + window_energy + * marker_energy + ) + + 1e-12 + ) + + normalized_correlation = ( + np.abs(correlation) + / denominator + ) + + start_symbol_index = int( + np.argmax( + normalized_correlation + ) + ) + + correlation_score = float( + normalized_correlation[ + start_symbol_index + ] + ) + + complex_correlation = correlation[ + start_symbol_index + ] + + if ( + best_result is None + or correlation_score + > best_result["score"] + ): + best_result = { + "score": correlation_score, + "sample_phase": sample_phase, + "start_symbol_index": ( + start_symbol_index + ), + "symbol_samples": ( + symbol_samples + ), + "correlation": correlation, + "normalized_correlation": ( + normalized_correlation + ), + "complex_correlation": ( + complex_correlation + ), + } + + if best_result is None: + raise RuntimeError( + "Не удалось выполнить поиск радиокадра" + ) + + return best_result + + +# ============================================================ +# Загрузка IQ из Lab018 +# ============================================================ + +if not INPUT_IQ_PATH.exists(): + raise FileNotFoundError( + f"Не найден IQ-файл: {INPUT_IQ_PATH}. " + "Сначала необходимо выполнить Lab018." + ) + +transmitted_iq = np.load( + INPUT_IQ_PATH +) + +if transmitted_iq.ndim != 1: + raise ValueError( + "IQ-массив должен быть одномерным" + ) + +if not np.iscomplexobj( + transmitted_iq +): + raise ValueError( + "Файл не содержит комплексные IQ-сэмплы" + ) + + +# ============================================================ +# Имитация неизвестных параметров канала +# ============================================================ + +phase_offset_radians = np.deg2rad( + TEST_PHASE_OFFSET_DEGREES +) + +phase_rotated_iq = ( + transmitted_iq + * np.exp( + 1j * phase_offset_radians + ) +) + +delayed_iq = np.concatenate( + [ + np.zeros( + TEST_SAMPLE_DELAY, + dtype=np.complex64, + ), + phase_rotated_iq.astype( + np.complex64 + ), + ] +) + +received_iq = delayed_iq + + +# ============================================================ +# Согласованный RRC-фильтр +# ============================================================ + +rrc_taps = root_raised_cosine_taps( + rolloff=RRC_ROLLOFF, + samples_per_symbol=SAMPLES_PER_SYMBOL, + span_symbols=RRC_SPAN_SYMBOLS, +) + +matched_iq = np.convolve( + received_iq, + rrc_taps, + mode="full", +) + + +# ============================================================ +# Поиск PREAMBLE + RADIO SYNC +# ============================================================ + +marker_bits, marker_symbols = ( + build_frame_marker() +) + +search_result = find_radio_frame( + matched_iq=matched_iq, + marker_symbols=marker_symbols, + samples_per_symbol=SAMPLES_PER_SYMBOL, +) + +correlation_score = search_result[ + "score" +] + +selected_sample_phase = search_result[ + "sample_phase" +] + +frame_start_symbol = search_result[ + "start_symbol_index" +] + +symbol_samples_before_correction = ( + search_result["symbol_samples"] +) + +complex_correlation = search_result[ + "complex_correlation" +] + + +# ============================================================ +# Оценка и компенсация фазы +# ============================================================ + +estimated_phase_radians = np.angle( + complex_correlation +) + +estimated_phase_degrees = np.rad2deg( + estimated_phase_radians +) + +phase_correction = np.exp( + -1j * estimated_phase_radians +) + +corrected_symbol_samples = ( + symbol_samples_before_correction + * phase_correction +) + + +# ============================================================ +# Демодуляция +# ============================================================ + +all_received_bits = bpsk_demodulate( + corrected_symbol_samples +) + +available_frame_bits = all_received_bits[ + frame_start_symbol: +] + + +# ============================================================ +# Проверка преамбулы +# ============================================================ + +received_marker_bits = available_frame_bits[ + :len(marker_bits) +] + +marker_bit_errors = int( + np.count_nonzero( + received_marker_bits + != marker_bits + ) +) + + +# ============================================================ +# Чтение радиозаголовка +# ============================================================ + +RADIO_HEADER_BIT_COUNT = 32 + +radio_header_start = ( + PREAMBLE_BIT_COUNT +) + +radio_header_end = ( + radio_header_start + + RADIO_HEADER_BIT_COUNT +) + +if len(available_frame_bits) < radio_header_end: + raise RuntimeError( + "Недостаточно битов для чтения радиозаголовка" + ) + +received_radio_header_bits = ( + available_frame_bits[ + radio_header_start: + radio_header_end + ] +) + +received_radio_header = bits_to_bytes( + received_radio_header_bits +) + +( + received_radio_sync, + protocol_packet_length, +) = struct.unpack( + ">HH", + received_radio_header, +) + + +# ============================================================ +# Извлечение внутреннего пакета +# ============================================================ + +if received_radio_sync != RADIO_SYNC_WORD: + raise RuntimeError( + "Получено неверное радиосинхрослово: " + f"0x{received_radio_sync:04X}" + ) + +protocol_packet_bit_count = ( + protocol_packet_length * 8 +) + +protocol_packet_start = ( + radio_header_end +) + +protocol_packet_end = ( + protocol_packet_start + + protocol_packet_bit_count +) + +if len(available_frame_bits) < protocol_packet_end: + raise RuntimeError( + "Принятый радиокадр короче значения LENGTH" + ) + +received_protocol_packet_bits = ( + available_frame_bits[ + protocol_packet_start: + protocol_packet_end + ] +) + +received_protocol_packet = bits_to_bytes( + received_protocol_packet_bits +) + +RECOVERED_PACKET_PATH.write_bytes( + received_protocol_packet +) + + +# ============================================================ +# Разбор внутреннего пакета +# ============================================================ + +parsed_packet = parse_packet( + received_protocol_packet +) + +restored_message = ( + parsed_packet.payload.decode( + "utf-8" + ) +) + + +# ============================================================ +# Вывод +# ============================================================ + +print( + "=== Lab019. Программный BPSK-приёмник ===" +) + +print("\nВходной IQ-файл:") + +print(INPUT_IQ_PATH) + +print("\nКоличество входных IQ-сэмплов:") + +print(len(transmitted_iq)) + +print("\nИскусственно внесено:") + +print( + "Задержка:", + TEST_SAMPLE_DELAY, + "сэмплов", +) + +print( + "Поворот фазы:", + TEST_PHASE_OFFSET_DEGREES, + "градусов", +) + + +print("\nРезультат синхронизации:") + +print( + "Найденная фаза дискретизации:", + selected_sample_phase, + "из", + SAMPLES_PER_SYMBOL, +) + +print( + "Начало кадра в потоке символов:", + frame_start_symbol, +) + +print( + "Корреляционная оценка:", + f"{correlation_score:.6f}", +) + +print( + "Оценённый фазовый поворот:", + f"{estimated_phase_degrees:.2f}", + "градуса", +) + +print( + "Ошибки в PREAMBLE + SYNC:", + marker_bit_errors, +) + + +print("\nРадиозаголовок:") + +print( + "RADIO SYNC:", + f"0x{received_radio_sync:04X}", +) + +print( + "LENGTH:", + protocol_packet_length, + "байт", +) + + +print("\nВнутренний пакет:") + +print( + "Версия:", + parsed_packet.version, +) + +print( + "Тип сообщения:", + parsed_packet.message_type, +) + +print( + "Порядковый номер:", + parsed_packet.sequence_number, +) + +print( + "Размер PAYLOAD:", + len(parsed_packet.payload), + "байт", +) + + +print("\nВосстановленное сообщение:") + +print(restored_message) + + +print("\nВосстановленный бинарный пакет:") + +print(RECOVERED_PACKET_PATH) + + +# ============================================================ +# Графики +# ============================================================ + +figure, axes = plt.subplots( + 2, + 2, + figsize=(13, 10), +) + + +# ------------------------------------------------------------ +# 1. Корреляция +# ------------------------------------------------------------ + +normalized_correlation = search_result[ + "normalized_correlation" +] + +axes[0, 0].plot( + normalized_correlation +) + +axes[0, 0].axvline( + frame_start_symbol, + linestyle="--", +) + +axes[0, 0].set_xlabel( + "Предполагаемое начало маркера, символ" +) + +axes[0, 0].set_ylabel( + "Нормированная корреляция" +) + +axes[0, 0].set_title( + "Поиск PREAMBLE + RADIO SYNC" +) + +axes[0, 0].grid( + True +) + + +# ------------------------------------------------------------ +# 2. Созвездие до коррекции +# ------------------------------------------------------------ + +marker_start = frame_start_symbol + +marker_end = ( + marker_start + + len(marker_symbols) +) + +marker_samples_before = ( + symbol_samples_before_correction[ + marker_start: + marker_end + ] +) + +axes[0, 1].scatter( + marker_samples_before.real, + marker_samples_before.imag, + s=20, +) + +axes[0, 1].axhline( + 0.0, + linewidth=1, +) + +axes[0, 1].axvline( + 0.0, + linewidth=1, +) + +axes[0, 1].set_xlabel( + "I" +) + +axes[0, 1].set_ylabel( + "Q" +) + +axes[0, 1].set_title( + "BPSK до компенсации фазы" +) + +axes[0, 1].axis( + "equal" +) + +axes[0, 1].grid( + True +) + + +# ------------------------------------------------------------ +# 3. Созвездие после коррекции +# ------------------------------------------------------------ + +marker_samples_after = ( + corrected_symbol_samples[ + marker_start: + marker_end + ] +) + +axes[1, 0].scatter( + marker_samples_after.real, + marker_samples_after.imag, + s=20, +) + +axes[1, 0].axhline( + 0.0, + linewidth=1, +) + +axes[1, 0].axvline( + 0.0, + linewidth=1, +) + +axes[1, 0].set_xlabel( + "I" +) + +axes[1, 0].set_ylabel( + "Q" +) + +axes[1, 0].set_title( + "BPSK после компенсации фазы" +) + +axes[1, 0].axis( + "equal" +) + +axes[1, 0].grid( + True +) + + +# ------------------------------------------------------------ +# 4. Восстановленные биты кадра +# ------------------------------------------------------------ + +received_frame_bits = available_frame_bits[ + :protocol_packet_end +] + +axes[1, 1].step( + np.arange( + len(received_frame_bits) + ), + received_frame_bits, + where="post", +) + +axes[1, 1].axvline( + PREAMBLE_BIT_COUNT, + linestyle="--", +) + +axes[1, 1].axvline( + radio_header_end, + linestyle="--", +) + +axes[1, 1].set_xlabel( + "Номер бита" +) + +axes[1, 1].set_ylabel( + "Бит" +) + +axes[1, 1].set_title( + "Восстановленный радиокадр" +) + +axes[1, 1].set_ylim( + -0.2, + 1.2, +) + +axes[1, 1].grid( + True +) + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Отчёт +# ============================================================ + +report_lines = [ + "Lab019. BPSK receiver", + "", + f"Input IQ: {INPUT_IQ_PATH}", + f"Artificial sample delay: {TEST_SAMPLE_DELAY}", + ( + "Artificial phase offset: " + f"{TEST_PHASE_OFFSET_DEGREES:.2f} deg" + ), + ( + "Detected sample phase: " + f"{selected_sample_phase}" + ), + ( + "Detected frame start symbol: " + f"{frame_start_symbol}" + ), + ( + "Correlation score: " + f"{correlation_score:.6f}" + ), + ( + "Estimated phase: " + f"{estimated_phase_degrees:.2f} deg" + ), + ( + "Marker bit errors: " + f"{marker_bit_errors}" + ), + ( + "Radio sync: " + f"0x{received_radio_sync:04X}" + ), + ( + "Protocol packet length: " + f"{protocol_packet_length} bytes" + ), + ( + "Sequence number: " + f"{parsed_packet.sequence_number}" + ), + f"Restored message: {restored_message}", +] + +REPORT_PATH.write_text( + "\n".join( + report_lines + ), + encoding="utf-8", +) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +assert received_radio_sync == RADIO_SYNC_WORD + +assert protocol_packet_length == len( + received_protocol_packet +) + +assert marker_bit_errors == 0 + +assert parsed_packet.message_type == ( + MESSAGE_TYPE_TEXT +) + +assert parsed_packet.sequence_number == ( + EXPECTED_SEQUENCE_NUMBER +) + +assert restored_message == EXPECTED_MESSAGE + +assert abs( + estimated_phase_degrees + - TEST_PHASE_OFFSET_DEGREES +) < 1.0 + +assert correlation_score > 0.95 + +assert GRAPH_PATH.exists() +assert REPORT_PATH.exists() +assert RECOVERED_PACKET_PATH.exists() + + +print("\nГрафик приёмника:") + +print(GRAPH_PATH) + +print("\nОтчёт:") + +print(REPORT_PATH) + +print( + "\nПроверка пройдена: " + "радиокадр синхронизирован, " + "BPSK демодулирован, " + "внутренний пакет восстановлен и прошёл CRC." +) diff --git a/Projects/SDR_Rover/tests/lab020_bpsk_frame_error_rate.py b/Projects/SDR_Rover/tests/lab020_bpsk_frame_error_rate.py new file mode 100644 index 0000000..7db38ae --- /dev/null +++ b/Projects/SDR_Rover/tests/lab020_bpsk_frame_error_rate.py @@ -0,0 +1,1494 @@ +""" +Lab020. Frame Error Rate полного BPSK-радиокадра. + +Программа многократно передаёт IQ-радиокадр из Lab018 +при разных значениях Eb/N0. + +В каждой попытке случайным образом изменяются: + +- задержка начала сигнала; +- постоянный фазовый поворот BPSK; +- реализация AWGN-шума. + +Приёмник выполняет: + +1. Согласованную RRC-фильтрацию. +2. Поиск PREAMBLE + RADIO SYNC. +3. Выбор фазы символьной дискретизации. +4. Оценку и компенсацию фазового поворота. +5. Демодуляцию BPSK. +6. Чтение радиозаголовка. +7. Извлечение внутреннего пакета. +8. Проверку структуры и CRC-32. +9. Восстановление исходного сообщения. + +Результат: + +FER — Frame Error Rate, доля радиокадров, +которые не удалось полностью восстановить. +""" + +from csv import DictWriter +from math import erfc, expm1, log1p, sqrt +from pathlib import Path +import struct + +import matplotlib.pyplot as plt +import numpy as np +from scipy.signal import fftconvolve + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_TEXT, + PacketError, + parse_packet, +) + + +# ============================================================ +# Параметры радиокадра из Lab018 +# ============================================================ + +RADIO_SYNC_WORD = 0xD391 + +PREAMBLE_BIT_COUNT = 64 + +SAMPLES_PER_SYMBOL = 32 + +RRC_ROLLOFF = 0.35 +RRC_SPAN_SYMBOLS = 10 + +# Полный радиокадр: +# +# PREAMBLE 64 бита +# RADIO HEADER 32 бита +# INNER PACKET 224 бита +# +# Итого 320 бит +RADIO_FRAME_BIT_COUNT = 320 + + +# ============================================================ +# Контрольные данные +# ============================================================ + +EXPECTED_MESSAGE = "ПРИВЕТ SDR" +EXPECTED_SEQUENCE_NUMBER = 18 + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +EB_N0_VALUES_DB = [ + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + 10.0, + 12.0, +] + +TRIALS_PER_EB_N0 = 100 + +RANDOM_SEED = 2026 + +# Максимальная случайная задержка начала кадра. +MAX_RANDOM_DELAY_SAMPLES = ( + 2 * SAMPLES_PER_SYMBOL +) + +# Если нормированная корреляция ниже этого значения, +# считаем, что маркер радиокадра не обнаружен. +DETECTION_THRESHOLD = 0.55 + +# Защита от повреждённого поля LENGTH. +MAX_PROTOCOL_PACKET_SIZE = 4096 + + +# ============================================================ +# Пути +# ============================================================ + +INPUT_IQ_PATH = Path( + "data/processed/lab018/" + "lab018_bpsk_tx_iq.npy" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab020" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab020_frame_error_results.csv" +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab020_frame_error_rate.png" +) + +REPORT_PATH = ( + OUTPUT_DIRECTORY + / "lab020_frame_error_report.txt" +) + + +# ============================================================ +# Статусы одной попытки +# ============================================================ + +STATUS_SUCCESS = "SUCCESS" +STATUS_DETECTION_FAIL = "DETECTION_FAIL" +STATUS_HEADER_ERROR = "HEADER_ERROR" +STATUS_CRC_ERROR = "CRC_ERROR" +STATUS_PACKET_ERROR = "PACKET_ERROR" + + +# ============================================================ +# Преобразование bytes → bits +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать bytes в одномерный массив битов. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + return np.unpackbits( + np.frombuffer( + data, + dtype=np.uint8, + ) + ) + + +# ============================================================ +# Преобразование bits → bytes +# ============================================================ + +def bits_to_bytes( + bits: np.ndarray, +) -> bytes: + """ + Упаковать биты обратно в bytes. + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if len(bits) % 8 != 0: + raise ValueError( + "Количество битов должно быть кратно восьми" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + return np.packbits( + bits + ).tobytes() + + +# ============================================================ +# BPSK +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в опорные BPSK-символы. + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + symbols = ( + 2.0 * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +def bpsk_demodulate( + symbols: np.ndarray, +) -> np.ndarray: + """ + Демодулировать BPSK по знаку компоненты I. + """ + + return ( + symbols.real >= 0.0 + ).astype(np.uint8) + + +# ============================================================ +# Root Raised Cosine-фильтр +# ============================================================ + +def root_raised_cosine_taps( + rolloff: float, + samples_per_symbol: int, + span_symbols: int, +) -> np.ndarray: + """ + Рассчитать коэффициенты RRC-фильтра. + + Параметры совпадают с Lab018 и Lab019. + """ + + if not 0.0 < rolloff <= 1.0: + raise ValueError( + "rolloff должен находиться в диапазоне 0...1" + ) + + if samples_per_symbol <= 0: + raise ValueError( + "samples_per_symbol должен быть положительным" + ) + + if span_symbols <= 0: + raise ValueError( + "span_symbols должен быть положительным" + ) + + if span_symbols % 2 != 0: + raise ValueError( + "span_symbols должен быть чётным" + ) + + half_sample_count = ( + span_symbols + * samples_per_symbol + // 2 + ) + + sample_indexes = np.arange( + -half_sample_count, + half_sample_count + 1, + dtype=np.float64, + ) + + time_values = ( + sample_indexes + / samples_per_symbol + ) + + taps = np.zeros_like( + time_values + ) + + beta = rolloff + + for index, time_value in enumerate( + time_values + ): + + if np.isclose( + time_value, + 0.0, + ): + taps[index] = ( + 1.0 + - beta + + 4.0 * beta / np.pi + ) + + continue + + if np.isclose( + abs(time_value), + 1.0 / (4.0 * beta), + ): + taps[index] = ( + beta + / np.sqrt(2.0) + * ( + ( + 1.0 + + 2.0 / np.pi + ) + * np.sin( + np.pi + / (4.0 * beta) + ) + + ( + 1.0 + - 2.0 / np.pi + ) + * np.cos( + np.pi + / (4.0 * beta) + ) + ) + ) + + continue + + numerator = ( + np.sin( + np.pi + * time_value + * (1.0 - beta) + ) + + ( + 4.0 + * beta + * time_value + * np.cos( + np.pi + * time_value + * (1.0 + beta) + ) + ) + ) + + denominator = ( + np.pi + * time_value + * ( + 1.0 + - ( + 4.0 + * beta + * time_value + ) ** 2 + ) + ) + + taps[index] = ( + numerator + / denominator + ) + + taps /= np.sqrt( + np.sum( + taps ** 2 + ) + ) + + return taps + + +# ============================================================ +# Маркер PREAMBLE + RADIO SYNC +# ============================================================ + +def build_frame_marker( +) -> tuple[np.ndarray, np.ndarray]: + """ + Сформировать биты и BPSK-символы маркера. + """ + + preamble_bits = np.tile( + np.array( + [1, 0], + dtype=np.uint8, + ), + PREAMBLE_BIT_COUNT // 2, + ) + + sync_bits = bytes_to_bits( + struct.pack( + ">H", + RADIO_SYNC_WORD, + ) + ) + + marker_bits = np.concatenate( + [ + preamble_bits, + sync_bits, + ] + ) + + marker_symbols = bpsk_modulate( + marker_bits + ) + + return marker_bits, marker_symbols + + +# ============================================================ +# Теоретический BER и идеальная FER +# ============================================================ + +def theoretical_bpsk_ber( + eb_n0_db: float, +) -> float: + """ + Теоретический BER когерентной BPSK в AWGN. + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + return 0.5 * erfc( + sqrt(eb_n0_linear) + ) + + +def ber_to_frame_error_rate( + ber: float, + frame_bit_count: int, +) -> float: + """ + Оценить FER при независимых битовых ошибках. + + FER = 1 - (1 - BER) ** N + """ + + if ber == 0.0: + return 0.0 + + return -expm1( + frame_bit_count + * log1p(-ber) + ) + + +# ============================================================ +# Добавление AWGN по заданному Eb/N0 +# ============================================================ + +def add_awgn_for_eb_n0( + clean_iq: np.ndarray, + signal_energy_per_bit: float, + eb_n0_db: float, + random_generator: np.random.Generator, +) -> np.ndarray: + """ + Добавить комплексный AWGN. + + signal_energy_per_bit рассчитывается по исходному + IQ-радиокадру как: + + сумма |IQ|² / количество передаваемых битов + + Полная комплексная дисперсия шума: + + noise_variance = Eb / (Eb/N0) + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + complex_noise_variance = ( + signal_energy_per_bit + / eb_n0_linear + ) + + component_sigma = sqrt( + complex_noise_variance / 2.0 + ) + + noise = component_sigma * ( + random_generator.standard_normal( + len(clean_iq) + ) + + 1j + * random_generator.standard_normal( + len(clean_iq) + ) + ) + + return clean_iq + noise + + +# ============================================================ +# Корреляционный поиск радиокадра +# ============================================================ + +def find_radio_frame( + matched_iq: np.ndarray, + marker_symbols: np.ndarray, + samples_per_symbol: int, +) -> dict: + """ + Перебрать все возможные фазы символьной дискретизации + и найти максимальную нормированную корреляцию. + """ + + marker_energy = float( + np.sum( + np.abs(marker_symbols) ** 2 + ) + ) + + best_result = None + + for sample_phase in range( + samples_per_symbol + ): + + symbol_samples = matched_iq[ + sample_phase::samples_per_symbol + ] + + if len(symbol_samples) < len( + marker_symbols + ): + continue + + correlation = np.correlate( + symbol_samples, + marker_symbols, + mode="valid", + ) + + window_energy = np.convolve( + np.abs(symbol_samples) ** 2, + np.ones( + len(marker_symbols) + ), + mode="valid", + ) + + normalized_correlation = ( + np.abs(correlation) + / ( + np.sqrt( + window_energy + * marker_energy + ) + + 1e-12 + ) + ) + + start_symbol_index = int( + np.argmax( + normalized_correlation + ) + ) + + score = float( + normalized_correlation[ + start_symbol_index + ] + ) + + if ( + best_result is None + or score > best_result["score"] + ): + best_result = { + "score": score, + "sample_phase": sample_phase, + "start_symbol_index": ( + start_symbol_index + ), + "symbol_samples": ( + symbol_samples + ), + "complex_correlation": ( + correlation[ + start_symbol_index + ] + ), + } + + if best_result is None: + raise RuntimeError( + "Корреляционный поиск не дал результата" + ) + + return best_result + + +# ============================================================ +# Приём одной реализации радиокадра +# ============================================================ + +def receive_one_frame( + received_iq: np.ndarray, + rrc_taps: np.ndarray, + marker_bits: np.ndarray, + marker_symbols: np.ndarray, +) -> dict: + """ + Выполнить полный приём одного радиокадра. + + Возвращает словарь со статусом и диагностикой. + """ + + matched_iq = fftconvolve( + received_iq, + rrc_taps, + mode="full", + ) + + search_result = find_radio_frame( + matched_iq=matched_iq, + marker_symbols=marker_symbols, + samples_per_symbol=SAMPLES_PER_SYMBOL, + ) + + correlation_score = search_result[ + "score" + ] + + if correlation_score < DETECTION_THRESHOLD: + return { + "status": STATUS_DETECTION_FAIL, + "correlation_score": correlation_score, + "marker_bit_errors": None, + } + + symbol_samples = search_result[ + "symbol_samples" + ] + + frame_start_symbol = search_result[ + "start_symbol_index" + ] + + estimated_phase = np.angle( + search_result[ + "complex_correlation" + ] + ) + + corrected_symbols = ( + symbol_samples + * np.exp( + -1j * estimated_phase + ) + ) + + received_bits = bpsk_demodulate( + corrected_symbols + ) + + available_bits = received_bits[ + frame_start_symbol: + ] + + marker_bit_count = len( + marker_bits + ) + + if len(available_bits) < marker_bit_count: + return { + "status": STATUS_HEADER_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": None, + } + + received_marker_bits = available_bits[ + :marker_bit_count + ] + + marker_bit_errors = int( + np.count_nonzero( + received_marker_bits + != marker_bits + ) + ) + + radio_header_start = ( + PREAMBLE_BIT_COUNT + ) + + radio_header_end = ( + radio_header_start + 32 + ) + + if len(available_bits) < radio_header_end: + return { + "status": STATUS_HEADER_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + try: + radio_header = bits_to_bytes( + available_bits[ + radio_header_start: + radio_header_end + ] + ) + + ( + received_radio_sync, + protocol_packet_length, + ) = struct.unpack( + ">HH", + radio_header, + ) + + except (ValueError, struct.error): + return { + "status": STATUS_HEADER_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + if received_radio_sync != RADIO_SYNC_WORD: + return { + "status": STATUS_HEADER_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + if not ( + 1 + <= protocol_packet_length + <= MAX_PROTOCOL_PACKET_SIZE + ): + return { + "status": STATUS_HEADER_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + protocol_packet_start = ( + radio_header_end + ) + + protocol_packet_end = ( + protocol_packet_start + + protocol_packet_length * 8 + ) + + if len(available_bits) < protocol_packet_end: + return { + "status": STATUS_HEADER_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + try: + protocol_packet = bits_to_bytes( + available_bits[ + protocol_packet_start: + protocol_packet_end + ] + ) + + except ValueError: + return { + "status": STATUS_PACKET_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + try: + parsed_packet = parse_packet( + protocol_packet + ) + + except CRCError: + return { + "status": STATUS_CRC_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + except PacketError: + return { + "status": STATUS_PACKET_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + try: + restored_message = ( + parsed_packet.payload.decode( + "utf-8" + ) + ) + + except UnicodeDecodeError: + return { + "status": STATUS_PACKET_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + if ( + parsed_packet.message_type + != MESSAGE_TYPE_TEXT + or parsed_packet.sequence_number + != EXPECTED_SEQUENCE_NUMBER + or restored_message + != EXPECTED_MESSAGE + ): + return { + "status": STATUS_PACKET_ERROR, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + return { + "status": STATUS_SUCCESS, + "correlation_score": correlation_score, + "marker_bit_errors": marker_bit_errors, + } + + +# ============================================================ +# Загрузка IQ из Lab018 +# ============================================================ + +if not INPUT_IQ_PATH.exists(): + raise FileNotFoundError( + f"Не найден файл: {INPUT_IQ_PATH}. " + "Сначала необходимо выполнить Lab018." + ) + +transmitted_iq = np.load( + INPUT_IQ_PATH +) + +if transmitted_iq.ndim != 1: + raise ValueError( + "IQ-массив должен быть одномерным" + ) + +if not np.iscomplexobj( + transmitted_iq +): + raise ValueError( + "Входной массив должен быть комплексным" + ) + +transmitted_iq = transmitted_iq.astype( + np.complex128 +) + + +# ============================================================ +# Подготовка приёмника +# ============================================================ + +rrc_taps = root_raised_cosine_taps( + rolloff=RRC_ROLLOFF, + samples_per_symbol=SAMPLES_PER_SYMBOL, + span_symbols=RRC_SPAN_SYMBOLS, +) + +marker_bits, marker_symbols = ( + build_frame_marker() +) + +signal_energy_per_bit = ( + np.sum( + np.abs(transmitted_iq) ** 2 + ) + / RADIO_FRAME_BIT_COUNT +) + + +# ============================================================ +# Основной эксперимент +# ============================================================ + +results = [] + +for eb_n0_index, eb_n0_db in enumerate( + EB_N0_VALUES_DB +): + + counters = { + STATUS_SUCCESS: 0, + STATUS_DETECTION_FAIL: 0, + STATUS_HEADER_ERROR: 0, + STATUS_CRC_ERROR: 0, + STATUS_PACKET_ERROR: 0, + } + + correlation_scores = [] + marker_error_counts = [] + + random_generator = np.random.default_rng( + RANDOM_SEED + + 1000 * eb_n0_index + ) + + for trial_index in range( + TRIALS_PER_EB_N0 + ): + + random_delay = int( + random_generator.integers( + 0, + MAX_RANDOM_DELAY_SAMPLES + 1, + ) + ) + + random_phase = ( + random_generator.uniform( + -np.pi, + np.pi, + ) + ) + + phase_rotated_iq = ( + transmitted_iq + * np.exp( + 1j * random_phase + ) + ) + + # Добавляем нули до и после кадра. + # После внесения AWGN эти участки превращаются + # в обычный шумовой фон. + clean_received_iq = np.concatenate( + [ + np.zeros( + random_delay, + dtype=np.complex128, + ), + phase_rotated_iq, + np.zeros( + 2 * SAMPLES_PER_SYMBOL, + dtype=np.complex128, + ), + ] + ) + + noisy_received_iq = add_awgn_for_eb_n0( + clean_iq=clean_received_iq, + signal_energy_per_bit=( + signal_energy_per_bit + ), + eb_n0_db=eb_n0_db, + random_generator=random_generator, + ) + + attempt_result = receive_one_frame( + received_iq=noisy_received_iq, + rrc_taps=rrc_taps, + marker_bits=marker_bits, + marker_symbols=marker_symbols, + ) + + status = attempt_result[ + "status" + ] + + counters[status] += 1 + + correlation_scores.append( + attempt_result[ + "correlation_score" + ] + ) + + marker_bit_errors = attempt_result[ + "marker_bit_errors" + ] + + if marker_bit_errors is not None: + marker_error_counts.append( + marker_bit_errors + ) + + success_count = counters[ + STATUS_SUCCESS + ] + + frame_error_count = ( + TRIALS_PER_EB_N0 + - success_count + ) + + experimental_fer = ( + frame_error_count + / TRIALS_PER_EB_N0 + ) + + theoretical_ber = theoretical_bpsk_ber( + eb_n0_db + ) + + ideal_fer = ber_to_frame_error_rate( + ber=theoretical_ber, + frame_bit_count=( + RADIO_FRAME_BIT_COUNT + ), + ) + + mean_correlation = float( + np.mean( + correlation_scores + ) + ) + + if marker_error_counts: + mean_marker_bit_errors = float( + np.mean( + marker_error_counts + ) + ) + else: + mean_marker_bit_errors = float( + "nan" + ) + + results.append( + { + "eb_n0_db": eb_n0_db, + "trials": TRIALS_PER_EB_N0, + "success_count": success_count, + "detection_fail_count": counters[ + STATUS_DETECTION_FAIL + ], + "header_error_count": counters[ + STATUS_HEADER_ERROR + ], + "crc_error_count": counters[ + STATUS_CRC_ERROR + ], + "packet_error_count": counters[ + STATUS_PACKET_ERROR + ], + "experimental_fer": experimental_fer, + "theoretical_ber": theoretical_ber, + "ideal_fer": ideal_fer, + "mean_correlation": mean_correlation, + "mean_marker_bit_errors": ( + mean_marker_bit_errors + ), + } + ) + + +# ============================================================ +# Вывод таблицы +# ============================================================ + +print( + "=== Lab020. FER полного BPSK-радиокадра ===" +) + +print("\nКоличество попыток на точку:") + +print(TRIALS_PER_EB_N0) + +print("\nРезультаты:") + +print( + f"{'Eb/N0':>9}" + f"{'Успех':>9}" + f"{'Нет марк.':>11}" + f"{'Загол.':>9}" + f"{'CRC':>7}" + f"{'Пакет':>8}" + f"{'FER':>10}" + f"{'Идеал FER':>13}" + f"{'Коррел.':>11}" +) + +print("-" * 87) + +for result in results: + + print( + f"{result['eb_n0_db']:>6.1f} дБ" + f"{result['success_count']:>9}" + f"{result['detection_fail_count']:>11}" + f"{result['header_error_count']:>9}" + f"{result['crc_error_count']:>7}" + f"{result['packet_error_count']:>8}" + f"{result['experimental_fer']:>10.3f}" + f"{result['ideal_fer']:>13.3f}" + f"{result['mean_correlation']:>11.3f}" + ) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + writer = DictWriter( + csv_file, + fieldnames=list( + results[0].keys() + ), + ) + + writer.writeheader() + writer.writerows(results) + + +# ============================================================ +# Подготовка данных графика +# ============================================================ + +eb_n0_values = np.array( + [ + result["eb_n0_db"] + for result in results + ], + dtype=np.float64, +) + +experimental_fer_values = np.array( + [ + result["experimental_fer"] + for result in results + ], + dtype=np.float64, +) + +ideal_fer_values = np.array( + [ + result["ideal_fer"] + for result in results + ], + dtype=np.float64, +) + +measurement_floor = ( + 0.5 / TRIALS_PER_EB_N0 +) + +experimental_fer_for_plot = np.maximum( + experimental_fer_values, + measurement_floor, +) + + +# ============================================================ +# Графики +# ============================================================ + +figure, axes = plt.subplots( + 2, + 1, + figsize=(11, 10), +) + + +# ------------------------------------------------------------ +# 1. FER +# ------------------------------------------------------------ + +axes[0].semilogy( + eb_n0_values, + ideal_fer_values, + marker="o", + label=( + "Идеальная FER по BER " + "при идеальной синхронизации" + ), +) + +axes[0].semilogy( + eb_n0_values, + experimental_fer_for_plot, + marker="s", + linestyle="--", + label="Экспериментальная FER приёмника", +) + +axes[0].axhline( + measurement_floor, + linestyle=":", + label=( + "Предел измерения " + f"{measurement_floor:.3f}" + ), +) + +axes[0].set_xlabel( + "Eb/N0, дБ" +) + +axes[0].set_ylabel( + "Frame Error Rate" +) + +axes[0].set_title( + "Устойчивость полного BPSK-радиокадра" +) + +axes[0].grid( + True, + which="both", +) + +axes[0].legend() + + +# ------------------------------------------------------------ +# 2. Классификация результатов +# ------------------------------------------------------------ + +success_rates = [ + result["success_count"] + / TRIALS_PER_EB_N0 + for result in results +] + +detection_fail_rates = [ + result["detection_fail_count"] + / TRIALS_PER_EB_N0 + for result in results +] + +header_error_rates = [ + result["header_error_count"] + / TRIALS_PER_EB_N0 + for result in results +] + +crc_error_rates = [ + result["crc_error_count"] + / TRIALS_PER_EB_N0 + for result in results +] + +packet_error_rates = [ + result["packet_error_count"] + / TRIALS_PER_EB_N0 + for result in results +] + +axes[1].plot( + eb_n0_values, + success_rates, + marker="o", + label="Успешно", +) + +axes[1].plot( + eb_n0_values, + detection_fail_rates, + marker="o", + label="Маркер не обнаружен", +) + +axes[1].plot( + eb_n0_values, + header_error_rates, + marker="o", + label="Ошибка радиозаголовка", +) + +axes[1].plot( + eb_n0_values, + crc_error_rates, + marker="o", + label="Ошибка CRC", +) + +axes[1].plot( + eb_n0_values, + packet_error_rates, + marker="o", + label="Ошибка структуры пакета", +) + +axes[1].set_xlabel( + "Eb/N0, дБ" +) + +axes[1].set_ylabel( + "Доля попыток" +) + +axes[1].set_ylim( + -0.03, + 1.03, +) + +axes[1].set_title( + "Причины потери радиокадров" +) + +axes[1].grid( + True +) + +axes[1].legend() + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Текстовый отчёт +# ============================================================ + +report_lines = [ + "Lab020. Full BPSK frame error rate", + "", + ( + "Trials per Eb/N0: " + f"{TRIALS_PER_EB_N0}" + ), + ( + "Detection threshold: " + f"{DETECTION_THRESHOLD:.3f}" + ), + ( + "Frame bit count: " + f"{RADIO_FRAME_BIT_COUNT}" + ), + "", +] + +for result in results: + + report_lines.extend( + [ + f"Eb/N0: {result['eb_n0_db']:.1f} dB", + ( + " Success: " + f"{result['success_count']}" + ), + ( + " Detection fail: " + f"{result['detection_fail_count']}" + ), + ( + " Header error: " + f"{result['header_error_count']}" + ), + ( + " CRC error: " + f"{result['crc_error_count']}" + ), + ( + " Packet error: " + f"{result['packet_error_count']}" + ), + ( + " Experimental FER: " + f"{result['experimental_fer']:.6f}" + ), + ( + " Ideal FER: " + f"{result['ideal_fer']:.6f}" + ), + "", + ] + ) + +REPORT_PATH.write_text( + "\n".join( + report_lines + ), + encoding="utf-8", +) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +result_at_4_db = next( + result + for result in results + if result["eb_n0_db"] == 4.0 +) + +result_at_12_db = next( + result + for result in results + if result["eb_n0_db"] == 12.0 +) + +assert sum( + [ + result[ + "success_count" + ], + result[ + "detection_fail_count" + ], + result[ + "header_error_count" + ], + result[ + "crc_error_count" + ], + result[ + "packet_error_count" + ], + ] +) == TRIALS_PER_EB_N0 + +assert ( + result_at_12_db["success_count"] + > result_at_4_db["success_count"] +) + +assert ( + result_at_12_db["experimental_fer"] + < result_at_4_db["experimental_fer"] +) + +assert CSV_PATH.exists() +assert GRAPH_PATH.exists() +assert REPORT_PATH.exists() + + +print("\nCSV:") + +print(CSV_PATH) + +print("\nГрафик:") + +print(GRAPH_PATH) + +print("\nОтчёт:") + +print(REPORT_PATH) + +print( + "\nПроверка пройдена: " + "измерена FER полного BPSK-радиокадра." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab021_frequency_offset_correction.py b/Projects/SDR_Rover/tests/lab021_frequency_offset_correction.py new file mode 100644 index 0000000..82b7578 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab021_frequency_offset_correction.py @@ -0,0 +1,1483 @@ +""" +Lab021. Оценка и компенсация частотного рассогласования BPSK. + +Программа: + +1. Загружает IQ-радиокадр из Lab018. +2. Добавляет постоянный фазовый поворот. +3. Добавляет заданное частотное рассогласование. +4. Добавляет неизвестную задержку. +5. Выполняет согласованную RRC-фильтрацию. +6. Ищет PREAMBLE + RADIO SYNC. +7. По известному маркеру оценивает: + - начало кадра; + - фазу символьной дискретизации; + - начальный фазовый поворот; + - частотную ошибку. +8. Сравнивает приём: + - только с постоянной фазовой коррекцией; + - с фазовой и частотной коррекцией. +9. Восстанавливает внутренний пакет и проверяет CRC. + +Шум в этой лабораторной не добавляется. +Изучается только влияние частотной ошибки. +""" + +from pathlib import Path +import struct + +import matplotlib.pyplot as plt +import numpy as np +from numpy.lib.stride_tricks import sliding_window_view +from scipy.signal import fftconvolve + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_TEXT, + PacketError, + parse_packet, +) + + +# ============================================================ +# Параметры радиокадра +# ============================================================ + +RADIO_SYNC_WORD = 0xD391 + +PREAMBLE_BIT_COUNT = 64 + +SAMPLES_PER_SYMBOL = 32 + +SYMBOL_RATE = 20_000 + +SAMPLE_RATE = ( + SYMBOL_RATE + * SAMPLES_PER_SYMBOL +) + +RRC_ROLLOFF = 0.35 +RRC_SPAN_SYMBOLS = 10 + + +# ============================================================ +# Контрольные данные +# ============================================================ + +EXPECTED_MESSAGE = "ПРИВЕТ SDR" +EXPECTED_SEQUENCE_NUMBER = 18 + + +# ============================================================ +# Искусственные искажения +# ============================================================ + +TEST_SAMPLE_DELAY = 11 + +TEST_PHASE_OFFSET_DEGREES = 37.0 + +FREQUENCY_OFFSETS_HZ = [ + 0.0, + 10.0, + 25.0, + 50.0, + 100.0, + 250.0, + 500.0, + 1000.0, +] + + +# ============================================================ +# Пути +# ============================================================ + +INPUT_IQ_PATH = Path( + "data/processed/lab018/" + "lab018_bpsk_tx_iq.npy" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab021" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab021_frequency_offset.png" +) + +REPORT_PATH = ( + OUTPUT_DIRECTORY + / "lab021_frequency_offset_report.txt" +) + + +# ============================================================ +# Статусы приёма +# ============================================================ + +STATUS_SUCCESS = "SUCCESS" +STATUS_HEADER_ERROR = "HEADER ERROR" +STATUS_CRC_ERROR = "CRC ERROR" +STATUS_PACKET_ERROR = "PACKET ERROR" + + +# ============================================================ +# Bytes и bits +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать bytes в одномерный массив битов. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + return np.unpackbits( + np.frombuffer( + data, + dtype=np.uint8, + ) + ) + + +def bits_to_bytes( + bits: np.ndarray, +) -> bytes: + """ + Упаковать отдельные биты обратно в bytes. + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if len(bits) % 8 != 0: + raise ValueError( + "Количество битов должно быть кратно восьми" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + return np.packbits( + bits + ).tobytes() + + +# ============================================================ +# BPSK +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в BPSK-символы. + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + symbols = ( + 2.0 + * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +def bpsk_demodulate( + symbols: np.ndarray, +) -> np.ndarray: + """ + Демодулировать BPSK по знаку компоненты I. + """ + + return ( + symbols.real >= 0.0 + ).astype(np.uint8) + + +# ============================================================ +# Root Raised Cosine +# ============================================================ + +def root_raised_cosine_taps( + rolloff: float, + samples_per_symbol: int, + span_symbols: int, +) -> np.ndarray: + """ + Рассчитать коэффициенты RRC-фильтра. + """ + + if not 0.0 < rolloff <= 1.0: + raise ValueError( + "rolloff должен находиться в диапазоне 0...1" + ) + + if samples_per_symbol <= 0: + raise ValueError( + "samples_per_symbol должен быть положительным" + ) + + if span_symbols <= 0: + raise ValueError( + "span_symbols должен быть положительным" + ) + + if span_symbols % 2 != 0: + raise ValueError( + "span_symbols должен быть чётным" + ) + + half_sample_count = ( + span_symbols + * samples_per_symbol + // 2 + ) + + sample_indexes = np.arange( + -half_sample_count, + half_sample_count + 1, + dtype=np.float64, + ) + + time_values = ( + sample_indexes + / samples_per_symbol + ) + + taps = np.zeros_like( + time_values + ) + + beta = rolloff + + for index, time_value in enumerate( + time_values + ): + + if np.isclose( + time_value, + 0.0, + ): + taps[index] = ( + 1.0 + - beta + + 4.0 * beta / np.pi + ) + + continue + + if np.isclose( + abs(time_value), + 1.0 / (4.0 * beta), + ): + taps[index] = ( + beta + / np.sqrt(2.0) + * ( + ( + 1.0 + + 2.0 / np.pi + ) + * np.sin( + np.pi + / (4.0 * beta) + ) + + ( + 1.0 + - 2.0 / np.pi + ) + * np.cos( + np.pi + / (4.0 * beta) + ) + ) + ) + + continue + + numerator = ( + np.sin( + np.pi + * time_value + * (1.0 - beta) + ) + + ( + 4.0 + * beta + * time_value + * np.cos( + np.pi + * time_value + * (1.0 + beta) + ) + ) + ) + + denominator = ( + np.pi + * time_value + * ( + 1.0 + - ( + 4.0 + * beta + * time_value + ) ** 2 + ) + ) + + taps[index] = ( + numerator + / denominator + ) + + taps /= np.sqrt( + np.sum( + taps ** 2 + ) + ) + + return taps + + +# ============================================================ +# Маркер радиокадра +# ============================================================ + +def build_frame_marker( +) -> tuple[np.ndarray, np.ndarray]: + """ + Сформировать PREAMBLE + RADIO SYNC. + """ + + preamble_bits = np.tile( + np.array( + [1, 0], + dtype=np.uint8, + ), + PREAMBLE_BIT_COUNT // 2, + ) + + sync_bits = bytes_to_bits( + struct.pack( + ">H", + RADIO_SYNC_WORD, + ) + ) + + marker_bits = np.concatenate( + [ + preamble_bits, + sync_bits, + ] + ) + + marker_symbols = bpsk_modulate( + marker_bits + ) + + return marker_bits, marker_symbols + + +# ============================================================ +# Добавление частотного смещения +# ============================================================ + +def apply_frequency_offset( + iq_samples: np.ndarray, + frequency_offset_hz: float, + sample_rate: float, + phase_offset_degrees: float, +) -> np.ndarray: + """ + Добавить постоянную фазу и частотное рассогласование. + + В каждом следующем сэмпле фаза увеличивается на: + + 2π · Δf / Fs + """ + + sample_indexes = np.arange( + len(iq_samples), + dtype=np.float64, + ) + + initial_phase_radians = np.deg2rad( + phase_offset_degrees + ) + + phase_values = ( + initial_phase_radians + + 2.0 + * np.pi + * frequency_offset_hz + * sample_indexes + / sample_rate + ) + + return ( + iq_samples + * np.exp( + 1j * phase_values + ) + ) + + +# ============================================================ +# Поиск маркера и оценка CFO +# ============================================================ + +def find_frame_and_frequency_offset( + matched_iq: np.ndarray, + marker_symbols: np.ndarray, + samples_per_symbol: int, + symbol_rate: float, +) -> dict: + """ + Найти радиокадр и оценить частотную ошибку. + + Для каждого возможного положения маркера: + + 1. Умножаем принятые символы на известные + символы маркера. + + Это удаляет BPSK-модуляцию: + + received × marker + ≈ exp(j · phase) + + 2. Измеряем среднее изменение фазы + между соседними символами. + + 3. Компенсируем полученный наклон фазы. + + 4. Рассчитываем когерентную корреляцию. + """ + + marker_length = len( + marker_symbols + ) + + marker_energy = float( + np.sum( + np.abs(marker_symbols) ** 2 + ) + ) + + marker_indexes = np.arange( + marker_length, + dtype=np.float64, + ) + + best_result = None + + for sample_phase in range( + samples_per_symbol + ): + + symbol_samples = matched_iq[ + sample_phase::samples_per_symbol + ] + + if len(symbol_samples) < marker_length: + continue + + windows = sliding_window_view( + symbol_samples, + marker_length, + ) + + # Удаление известных знаков BPSK-маркера. + despread_windows = ( + windows + * marker_symbols[ + np.newaxis, + : + ] + ) + + # Изменение фазы между соседними символами. + adjacent_products = ( + despread_windows[:, 1:] + * np.conj( + despread_windows[:, :-1] + ) + ) + + phase_increments = np.angle( + np.sum( + adjacent_products, + axis=1, + ) + ) + + # Компенсация предполагаемой частотной ошибки. + frequency_compensation = np.exp( + -1j + * phase_increments[:, np.newaxis] + * marker_indexes[np.newaxis, :] + ) + + compensated_windows = ( + despread_windows + * frequency_compensation + ) + + coherent_sums = np.sum( + compensated_windows, + axis=1, + ) + + window_energy = np.sum( + np.abs(windows) ** 2, + axis=1, + ) + + normalized_scores = ( + np.abs(coherent_sums) + / ( + np.sqrt( + window_energy + * marker_energy + ) + + 1e-12 + ) + ) + + best_start_for_phase = int( + np.argmax( + normalized_scores + ) + ) + + best_score_for_phase = float( + normalized_scores[ + best_start_for_phase + ] + ) + + if ( + best_result is None + or best_score_for_phase + > best_result["score"] + ): + + selected_phase_increment = float( + phase_increments[ + best_start_for_phase + ] + ) + + selected_coherent_sum = ( + coherent_sums[ + best_start_for_phase + ] + ) + + estimated_frequency_offset_hz = ( + selected_phase_increment + * symbol_rate + / (2.0 * np.pi) + ) + + estimated_initial_phase = np.angle( + selected_coherent_sum + ) + + best_result = { + "score": best_score_for_phase, + "sample_phase": sample_phase, + "start_symbol_index": ( + best_start_for_phase + ), + "symbol_samples": symbol_samples, + "phase_increment": ( + selected_phase_increment + ), + "frequency_offset_hz": ( + estimated_frequency_offset_hz + ), + "initial_phase": ( + estimated_initial_phase + ), + } + + if best_result is None: + raise RuntimeError( + "Не удалось обнаружить радиокадр" + ) + + return best_result + + +# ============================================================ +# Восстановление внутреннего пакета +# ============================================================ + +def decode_radio_frame( + corrected_symbols: np.ndarray, + frame_start_symbol: int, + marker_bits: np.ndarray, +) -> dict: + """ + Демодулировать радиокадр и проверить внутренний пакет. + """ + + received_bits = bpsk_demodulate( + corrected_symbols + ) + + available_bits = received_bits[ + frame_start_symbol: + ] + + marker_bit_count = len( + marker_bits + ) + + if len(available_bits) < marker_bit_count: + return { + "status": STATUS_HEADER_ERROR, + "marker_errors": None, + "message": None, + } + + received_marker_bits = available_bits[ + :marker_bit_count + ] + + marker_errors = int( + np.count_nonzero( + received_marker_bits + != marker_bits + ) + ) + + radio_header_start = ( + PREAMBLE_BIT_COUNT + ) + + radio_header_end = ( + radio_header_start + 32 + ) + + if len(available_bits) < radio_header_end: + return { + "status": STATUS_HEADER_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + try: + radio_header = bits_to_bytes( + available_bits[ + radio_header_start: + radio_header_end + ] + ) + + ( + received_sync, + protocol_packet_length, + ) = struct.unpack( + ">HH", + radio_header, + ) + + except (ValueError, struct.error): + return { + "status": STATUS_HEADER_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + if received_sync != RADIO_SYNC_WORD: + return { + "status": STATUS_HEADER_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + if not 1 <= protocol_packet_length <= 4096: + return { + "status": STATUS_HEADER_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + protocol_packet_start = ( + radio_header_end + ) + + protocol_packet_end = ( + protocol_packet_start + + protocol_packet_length * 8 + ) + + if len(available_bits) < protocol_packet_end: + return { + "status": STATUS_HEADER_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + try: + protocol_packet = bits_to_bytes( + available_bits[ + protocol_packet_start: + protocol_packet_end + ] + ) + + parsed_packet = parse_packet( + protocol_packet + ) + + except CRCError: + return { + "status": STATUS_CRC_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + except (PacketError, ValueError): + return { + "status": STATUS_PACKET_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + try: + restored_message = ( + parsed_packet.payload.decode( + "utf-8" + ) + ) + + except UnicodeDecodeError: + return { + "status": STATUS_PACKET_ERROR, + "marker_errors": marker_errors, + "message": None, + } + + if ( + parsed_packet.message_type + != MESSAGE_TYPE_TEXT + or parsed_packet.sequence_number + != EXPECTED_SEQUENCE_NUMBER + or restored_message + != EXPECTED_MESSAGE + ): + return { + "status": STATUS_PACKET_ERROR, + "marker_errors": marker_errors, + "message": restored_message, + } + + return { + "status": STATUS_SUCCESS, + "marker_errors": marker_errors, + "message": restored_message, + } + + +# ============================================================ +# Загрузка IQ +# ============================================================ + +if not INPUT_IQ_PATH.exists(): + raise FileNotFoundError( + f"Не найден файл: {INPUT_IQ_PATH}. " + "Сначала необходимо выполнить Lab018." + ) + +transmitted_iq = np.load( + INPUT_IQ_PATH +).astype( + np.complex128 +) + +if transmitted_iq.ndim != 1: + raise ValueError( + "IQ-массив должен быть одномерным" + ) + + +# ============================================================ +# Подготовка приёмника +# ============================================================ + +rrc_taps = root_raised_cosine_taps( + rolloff=RRC_ROLLOFF, + samples_per_symbol=SAMPLES_PER_SYMBOL, + span_symbols=RRC_SPAN_SYMBOLS, +) + +marker_bits, marker_symbols = ( + build_frame_marker() +) + + +# ============================================================ +# Эксперимент +# ============================================================ + +results = [] + +worst_offset_diagnostic = None + +for frequency_offset_hz in FREQUENCY_OFFSETS_HZ: + + impaired_iq = apply_frequency_offset( + iq_samples=transmitted_iq, + frequency_offset_hz=frequency_offset_hz, + sample_rate=SAMPLE_RATE, + phase_offset_degrees=( + TEST_PHASE_OFFSET_DEGREES + ), + ) + + received_iq = np.concatenate( + [ + np.zeros( + TEST_SAMPLE_DELAY, + dtype=np.complex128, + ), + impaired_iq, + ] + ) + + matched_iq = fftconvolve( + received_iq, + rrc_taps, + mode="full", + ) + + search_result = ( + find_frame_and_frequency_offset( + matched_iq=matched_iq, + marker_symbols=marker_symbols, + samples_per_symbol=( + SAMPLES_PER_SYMBOL + ), + symbol_rate=SYMBOL_RATE, + ) + ) + + symbol_samples = search_result[ + "symbol_samples" + ] + + frame_start_symbol = search_result[ + "start_symbol_index" + ] + + estimated_frequency_hz = search_result[ + "frequency_offset_hz" + ] + + estimated_initial_phase = search_result[ + "initial_phase" + ] + + estimated_phase_increment = search_result[ + "phase_increment" + ] + + symbol_indexes = np.arange( + len(symbol_samples), + dtype=np.float64, + ) + + relative_symbol_indexes = ( + symbol_indexes + - frame_start_symbol + ) + + # -------------------------------------------------------- + # Приём без компенсации частотной ошибки + # -------------------------------------------------------- + + constant_phase_corrected = ( + symbol_samples + * np.exp( + -1j + * estimated_initial_phase + ) + ) + + result_without_cfo = decode_radio_frame( + corrected_symbols=( + constant_phase_corrected + ), + frame_start_symbol=( + frame_start_symbol + ), + marker_bits=marker_bits, + ) + + # -------------------------------------------------------- + # Полная фазовая и частотная коррекция + # -------------------------------------------------------- + + phase_model = ( + estimated_initial_phase + + estimated_phase_increment + * relative_symbol_indexes + ) + + frequency_corrected = ( + symbol_samples + * np.exp( + -1j * phase_model + ) + ) + + result_with_cfo = decode_radio_frame( + corrected_symbols=( + frequency_corrected + ), + frame_start_symbol=( + frame_start_symbol + ), + marker_bits=marker_bits, + ) + + frequency_error_hz = ( + estimated_frequency_hz + - frequency_offset_hz + ) + + results.append( + { + "true_frequency_hz": ( + frequency_offset_hz + ), + "estimated_frequency_hz": ( + estimated_frequency_hz + ), + "frequency_error_hz": ( + frequency_error_hz + ), + "correlation_score": ( + search_result["score"] + ), + "sample_phase": ( + search_result["sample_phase"] + ), + "frame_start_symbol": ( + frame_start_symbol + ), + "marker_errors_without": ( + result_without_cfo[ + "marker_errors" + ] + ), + "status_without": ( + result_without_cfo["status"] + ), + "marker_errors_with": ( + result_with_cfo[ + "marker_errors" + ] + ), + "status_with": ( + result_with_cfo["status"] + ), + } + ) + + if ( + frequency_offset_hz + == max(FREQUENCY_OFFSETS_HZ) + ): + marker_start = frame_start_symbol + marker_end = ( + marker_start + + len(marker_symbols) + ) + + marker_before = ( + symbol_samples[ + marker_start: + marker_end + ] + * marker_symbols + ) + + marker_after = ( + frequency_corrected[ + marker_start: + marker_end + ] + * marker_symbols + ) + + worst_offset_diagnostic = { + "before": marker_before, + "after": marker_after, + } + + +# ============================================================ +# Вывод +# ============================================================ + +print( + "=== Lab021. Компенсация частотной ошибки ===" +) + +print("\nПараметры:") + +print( + "Символьная скорость:", + SYMBOL_RATE, + "символов/с", +) + +print( + "Частота дискретизации:", + SAMPLE_RATE, + "сэмплов/с", +) + +print( + "Постоянный фазовый поворот:", + TEST_PHASE_OFFSET_DEGREES, + "градусов", +) + +print( + "Задержка:", + TEST_SAMPLE_DELAY, + "сэмплов", +) + + +print("\nРезультаты:") + +print( + f"{'Δf задано':>12}" + f"{'Δf оценено':>14}" + f"{'Ошибка':>12}" + f"{'Коррел.':>11}" + f"{'Без CFO':>16}" + f"{'С CFO':>16}" +) + +print("-" * 81) + +for result in results: + + print( + f"{result['true_frequency_hz']:>9.1f} Гц" + f"{result['estimated_frequency_hz']:>11.2f} Гц" + f"{result['frequency_error_hz']:>9.2f} Гц" + f"{result['correlation_score']:>11.4f}" + f"{result['status_without']:>16}" + f"{result['status_with']:>16}" + ) + + +# ============================================================ +# Графики +# ============================================================ + +true_frequencies = np.array( + [ + result["true_frequency_hz"] + for result in results + ], + dtype=np.float64, +) + +estimated_frequencies = np.array( + [ + result["estimated_frequency_hz"] + for result in results + ], + dtype=np.float64, +) + +marker_errors_without = np.array( + [ + ( + result["marker_errors_without"] + if result["marker_errors_without"] + is not None + else len(marker_bits) + ) + for result in results + ], + dtype=np.float64, +) + +marker_errors_with = np.array( + [ + ( + result["marker_errors_with"] + if result["marker_errors_with"] + is not None + else len(marker_bits) + ) + for result in results + ], + dtype=np.float64, +) + +success_without = np.array( + [ + result["status_without"] + == STATUS_SUCCESS + for result in results + ], + dtype=np.int32, +) + +success_with = np.array( + [ + result["status_with"] + == STATUS_SUCCESS + for result in results + ], + dtype=np.int32, +) + + +figure, axes = plt.subplots( + 2, + 2, + figsize=(13, 10), +) + + +# ------------------------------------------------------------ +# 1. Оценка частоты +# ------------------------------------------------------------ + +axes[0, 0].plot( + true_frequencies, + true_frequencies, + linestyle="--", + label="Идеальная оценка", +) + +axes[0, 0].plot( + true_frequencies, + estimated_frequencies, + marker="o", + label="Оценка приёмника", +) + +axes[0, 0].set_xlabel( + "Заданное смещение, Гц" +) + +axes[0, 0].set_ylabel( + "Оценённое смещение, Гц" +) + +axes[0, 0].set_title( + "Оценка частотного рассогласования" +) + +axes[0, 0].grid( + True +) + +axes[0, 0].legend() + + +# ------------------------------------------------------------ +# 2. Ошибки маркера +# ------------------------------------------------------------ + +axes[0, 1].plot( + true_frequencies, + marker_errors_without, + marker="o", + label="Без компенсации CFO", +) + +axes[0, 1].plot( + true_frequencies, + marker_errors_with, + marker="s", + label="После компенсации CFO", +) + +axes[0, 1].set_xlabel( + "Частотное смещение, Гц" +) + +axes[0, 1].set_ylabel( + "Ошибки PREAMBLE + SYNC" +) + +axes[0, 1].set_title( + "Ошибки известного маркера" +) + +axes[0, 1].grid( + True +) + +axes[0, 1].legend() + + +# ------------------------------------------------------------ +# 3. Успешный приём кадра +# ------------------------------------------------------------ + +axes[1, 0].plot( + true_frequencies, + success_without, + marker="o", + label="Без компенсации CFO", +) + +axes[1, 0].plot( + true_frequencies, + success_with, + marker="s", + label="После компенсации CFO", +) + +axes[1, 0].set_yticks( + [0, 1], + [ + "Ошибка", + "SUCCESS", + ], +) + +axes[1, 0].set_xlabel( + "Частотное смещение, Гц" +) + +axes[1, 0].set_ylabel( + "Результат приёма" +) + +axes[1, 0].set_title( + "Восстановление полного пакета" +) + +axes[1, 0].grid( + True +) + +axes[1, 0].legend() + + +# ------------------------------------------------------------ +# 4. Фаза маркера при максимальном CFO +# ------------------------------------------------------------ + +if worst_offset_diagnostic is None: + raise RuntimeError( + "Не сохранена диагностика максимального CFO" + ) + +phase_before = np.unwrap( + np.angle( + worst_offset_diagnostic["before"] + ) +) + +phase_after = np.unwrap( + np.angle( + worst_offset_diagnostic["after"] + ) +) + +axes[1, 1].plot( + phase_before, + label="До компенсации", +) + +axes[1, 1].plot( + phase_after, + label="После компенсации", +) + +axes[1, 1].set_xlabel( + "Номер символа маркера" +) + +axes[1, 1].set_ylabel( + "Развёрнутая фаза, рад" +) + +axes[1, 1].set_title( + "Фаза при Δf = " + f"{max(FREQUENCY_OFFSETS_HZ):.0f} Гц" +) + +axes[1, 1].grid( + True +) + +axes[1, 1].legend() + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Отчёт +# ============================================================ + +report_lines = [ + "Lab021. Frequency offset correction", + "", + f"Symbol rate: {SYMBOL_RATE}", + f"Sample rate: {SAMPLE_RATE}", + ( + "Phase offset: " + f"{TEST_PHASE_OFFSET_DEGREES:.2f} deg" + ), + ( + "Sample delay: " + f"{TEST_SAMPLE_DELAY}" + ), + "", +] + +for result in results: + + report_lines.extend( + [ + ( + "True frequency offset: " + f"{result['true_frequency_hz']:.2f} Hz" + ), + ( + "Estimated frequency offset: " + f"{result['estimated_frequency_hz']:.4f} Hz" + ), + ( + "Frequency estimation error: " + f"{result['frequency_error_hz']:.4f} Hz" + ), + ( + "Correlation score: " + f"{result['correlation_score']:.6f}" + ), + ( + "Status without correction: " + f"{result['status_without']}" + ), + ( + "Status with correction: " + f"{result['status_with']}" + ), + "", + ] + ) + +REPORT_PATH.write_text( + "\n".join( + report_lines + ), + encoding="utf-8", +) + + +# ============================================================ +# Проверки +# ============================================================ + +result_at_zero = next( + result + for result in results + if result["true_frequency_hz"] == 0.0 +) + +result_at_maximum = next( + result + for result in results + if ( + result["true_frequency_hz"] + == max(FREQUENCY_OFFSETS_HZ) + ) +) + +assert ( + result_at_zero["status_without"] + == STATUS_SUCCESS +) + +assert all( + result["status_with"] == STATUS_SUCCESS + for result in results +) + +assert abs( + result_at_maximum["frequency_error_hz"] +) < 5.0 + +assert ( + result_at_maximum["status_without"] + != STATUS_SUCCESS +) + +assert GRAPH_PATH.exists() +assert REPORT_PATH.exists() + + +print("\nГрафик:") + +print(GRAPH_PATH) + +print("\nОтчёт:") + +print(REPORT_PATH) + +print( + "\nПроверка пройдена: " + "частотное рассогласование оценено и компенсировано." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab022_noise_and_frequency_offset.py b/Projects/SDR_Rover/tests/lab022_noise_and_frequency_offset.py new file mode 100644 index 0000000..977296a --- /dev/null +++ b/Projects/SDR_Rover/tests/lab022_noise_and_frequency_offset.py @@ -0,0 +1,1716 @@ +""" +Lab022. Совместное действие AWGN и частотного рассогласования. + +Программа многократно передаёт BPSK-радиокадр из Lab018. + +В каждой попытке случайно выбираются: + +- задержка начала кадра; +- начальный фазовый поворот; +- частотное рассогласование; +- реализация AWGN-шума. + +Сравниваются два варианта приёма: + +1. Только компенсация постоянной фазы. +2. Компенсация постоянной фазы и CFO + (Carrier Frequency Offset). + +Для каждой комбинации Eb/N0 и максимального CFO +рассчитываются: + +- FER без компенсации CFO; +- FER после компенсации CFO; +- число необнаруженных кадров; +- средняя корреляционная оценка; +- средняя и среднеквадратичная ошибка оценки CFO. +""" + +from csv import DictWriter +from pathlib import Path +import struct + +import matplotlib.pyplot as plt +import numpy as np +from numpy.lib.stride_tricks import sliding_window_view +from scipy.signal import fftconvolve + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_TEXT, + PacketError, + parse_packet, +) + + +# ============================================================ +# Параметры радиокадра +# ============================================================ + +RADIO_SYNC_WORD = 0xD391 + +PREAMBLE_BIT_COUNT = 64 + +RADIO_FRAME_BIT_COUNT = 320 + +SAMPLES_PER_SYMBOL = 32 + +SYMBOL_RATE = 20_000 + +SAMPLE_RATE = ( + SYMBOL_RATE + * SAMPLES_PER_SYMBOL +) + +RRC_ROLLOFF = 0.35 +RRC_SPAN_SYMBOLS = 10 + + +# ============================================================ +# Контрольные данные внутреннего пакета +# ============================================================ + +EXPECTED_MESSAGE = "ПРИВЕТ SDR" +EXPECTED_SEQUENCE_NUMBER = 18 + + +# ============================================================ +# Настройки эксперимента +# ============================================================ + +EB_N0_VALUES_DB = [ + 6.0, + 7.0, + 8.0, + 10.0, +] + +# В каждой попытке реальный CFO выбирается случайно +# из диапазона: +# +# -MAX_ABS_CFO ... +MAX_ABS_CFO +MAX_ABS_CFO_VALUES_HZ = [ + 0.0, + 250.0, + 1000.0, +] + +TRIALS_PER_POINT = 30 + +RANDOM_SEED = 2026 + +MAX_RANDOM_DELAY_SAMPLES = ( + 2 + * SAMPLES_PER_SYMBOL +) + +# При меньшей оценке маркер считаем необнаруженным. +DETECTION_THRESHOLD = 0.45 + +# После грубой оценки CFO выполняется уточняющий поиск. +CFO_REFINEMENT_HALF_WIDTH_HZ = 300.0 +CFO_REFINEMENT_STEP_HZ = 1.0 + +MAX_PROTOCOL_PACKET_SIZE = 4096 + + +# ============================================================ +# Пути +# ============================================================ + +INPUT_IQ_PATH = Path( + "data/processed/lab018/" + "lab018_bpsk_tx_iq.npy" +) + +OUTPUT_DIRECTORY = Path( + "data/processed/lab022" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab022_noise_cfo_results.csv" +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab022_noise_cfo.png" +) + +REPORT_PATH = ( + OUTPUT_DIRECTORY + / "lab022_noise_cfo_report.txt" +) + + +# ============================================================ +# Статусы приёма +# ============================================================ + +STATUS_SUCCESS = "SUCCESS" +STATUS_DETECTION_FAIL = "DETECTION FAIL" +STATUS_HEADER_ERROR = "HEADER ERROR" +STATUS_CRC_ERROR = "CRC ERROR" +STATUS_PACKET_ERROR = "PACKET ERROR" + + +# ============================================================ +# Преобразования bytes и bits +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать bytes в массив битов. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + return np.unpackbits( + np.frombuffer( + data, + dtype=np.uint8, + ) + ) + + +def bits_to_bytes( + bits: np.ndarray, +) -> bytes: + """ + Упаковать биты обратно в bytes. + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if len(bits) % 8 != 0: + raise ValueError( + "Количество битов должно быть кратно восьми" + ) + + if not np.all( + (bits == 0) + | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + return np.packbits( + bits + ).tobytes() + + +# ============================================================ +# BPSK +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в BPSK-символы. + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + symbols = ( + 2.0 + * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +def bpsk_demodulate( + symbols: np.ndarray, +) -> np.ndarray: + """ + Демодулировать BPSK по знаку компоненты I. + """ + + return ( + symbols.real >= 0.0 + ).astype(np.uint8) + + +# ============================================================ +# Root Raised Cosine +# ============================================================ + +def root_raised_cosine_taps( + rolloff: float, + samples_per_symbol: int, + span_symbols: int, +) -> np.ndarray: + """ + Рассчитать коэффициенты RRC-фильтра. + """ + + if not 0.0 < rolloff <= 1.0: + raise ValueError( + "rolloff должен быть в диапазоне 0...1" + ) + + if samples_per_symbol <= 0: + raise ValueError( + "samples_per_symbol должен быть положительным" + ) + + if span_symbols <= 0: + raise ValueError( + "span_symbols должен быть положительным" + ) + + if span_symbols % 2 != 0: + raise ValueError( + "span_symbols должен быть чётным" + ) + + half_sample_count = ( + span_symbols + * samples_per_symbol + // 2 + ) + + sample_indexes = np.arange( + -half_sample_count, + half_sample_count + 1, + dtype=np.float64, + ) + + time_values = ( + sample_indexes + / samples_per_symbol + ) + + taps = np.zeros_like( + time_values + ) + + beta = rolloff + + for index, time_value in enumerate( + time_values + ): + + if np.isclose( + time_value, + 0.0, + ): + taps[index] = ( + 1.0 + - beta + + 4.0 * beta / np.pi + ) + + continue + + if np.isclose( + abs(time_value), + 1.0 / (4.0 * beta), + ): + taps[index] = ( + beta + / np.sqrt(2.0) + * ( + ( + 1.0 + + 2.0 / np.pi + ) + * np.sin( + np.pi + / (4.0 * beta) + ) + + ( + 1.0 + - 2.0 / np.pi + ) + * np.cos( + np.pi + / (4.0 * beta) + ) + ) + ) + + continue + + numerator = ( + np.sin( + np.pi + * time_value + * (1.0 - beta) + ) + + ( + 4.0 + * beta + * time_value + * np.cos( + np.pi + * time_value + * (1.0 + beta) + ) + ) + ) + + denominator = ( + np.pi + * time_value + * ( + 1.0 + - ( + 4.0 + * beta + * time_value + ) ** 2 + ) + ) + + taps[index] = ( + numerator + / denominator + ) + + taps /= np.sqrt( + np.sum( + taps ** 2 + ) + ) + + return taps + + +# ============================================================ +# Маркер PREAMBLE + RADIO SYNC +# ============================================================ + +def build_frame_marker( +) -> tuple[np.ndarray, np.ndarray]: + """ + Сформировать известный маркер радиокадра. + """ + + preamble_bits = np.tile( + np.array( + [1, 0], + dtype=np.uint8, + ), + PREAMBLE_BIT_COUNT // 2, + ) + + sync_bits = bytes_to_bits( + struct.pack( + ">H", + RADIO_SYNC_WORD, + ) + ) + + marker_bits = np.concatenate( + [ + preamble_bits, + sync_bits, + ] + ) + + marker_symbols = bpsk_modulate( + marker_bits + ) + + return marker_bits, marker_symbols + + +# ============================================================ +# Искажения канала +# ============================================================ + +def apply_frequency_offset( + iq_samples: np.ndarray, + frequency_offset_hz: float, + phase_offset_radians: float, +) -> np.ndarray: + """ + Добавить начальную фазу и частотное рассогласование. + """ + + sample_indexes = np.arange( + len(iq_samples), + dtype=np.float64, + ) + + phase_values = ( + phase_offset_radians + + 2.0 + * np.pi + * frequency_offset_hz + * sample_indexes + / SAMPLE_RATE + ) + + return ( + iq_samples + * np.exp( + 1j * phase_values + ) + ) + + +def add_awgn_for_eb_n0( + clean_iq: np.ndarray, + signal_energy_per_bit: float, + eb_n0_db: float, + random_generator: np.random.Generator, +) -> np.ndarray: + """ + Добавить комплексный AWGN для заданного Eb/N0. + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + complex_noise_variance = ( + signal_energy_per_bit + / eb_n0_linear + ) + + component_sigma = np.sqrt( + complex_noise_variance / 2.0 + ) + + noise = component_sigma * ( + random_generator.standard_normal( + len(clean_iq) + ) + + 1j + * random_generator.standard_normal( + len(clean_iq) + ) + ) + + return clean_iq + noise + + +# ============================================================ +# Поиск кадра и оценка CFO +# ============================================================ + +def find_frame_and_frequency_offset( + matched_iq: np.ndarray, + marker_symbols: np.ndarray, +) -> dict: + """ + Найти кадр и оценить частотное рассогласование. + + Алгоритм состоит из двух этапов. + + Этап 1: + грубая оценка CFO по межсимвольному + изменению фазы известного маркера. + + Этап 2: + уточняющий частотный поиск около + полученной грубой оценки. + """ + + marker_length = len( + marker_symbols + ) + + marker_indexes = np.arange( + marker_length, + dtype=np.float64, + ) + + marker_energy = float( + np.sum( + np.abs(marker_symbols) ** 2 + ) + ) + + best_result = None + + # -------------------------------------------------------- + # Грубый поиск по всем фазам дискретизации + # -------------------------------------------------------- + + for sample_phase in range( + SAMPLES_PER_SYMBOL + ): + + symbol_samples = matched_iq[ + sample_phase::SAMPLES_PER_SYMBOL + ] + + if len(symbol_samples) < marker_length: + continue + + windows = sliding_window_view( + symbol_samples, + marker_length, + ) + + # Удаляем известные знаки BPSK-маркера. + despread_windows = ( + windows + * marker_symbols[ + np.newaxis, + : + ] + ) + + adjacent_products = ( + despread_windows[:, 1:] + * np.conj( + despread_windows[:, :-1] + ) + ) + + coarse_phase_increments = np.angle( + np.sum( + adjacent_products, + axis=1, + ) + ) + + coarse_compensation = np.exp( + -1j + * coarse_phase_increments[ + :, np.newaxis + ] + * marker_indexes[ + np.newaxis, : + ] + ) + + coherent_sums = np.sum( + despread_windows + * coarse_compensation, + axis=1, + ) + + window_energy = np.sum( + np.abs(windows) ** 2, + axis=1, + ) + + scores = ( + np.abs(coherent_sums) + / ( + np.sqrt( + window_energy + * marker_energy + ) + + 1e-12 + ) + ) + + start_index = int( + np.argmax( + scores + ) + ) + + score = float( + scores[start_index] + ) + + if ( + best_result is None + or score > best_result["score"] + ): + best_result = { + "score": score, + "sample_phase": sample_phase, + "start_symbol_index": start_index, + "symbol_samples": symbol_samples, + "despread_marker": ( + despread_windows[ + start_index + ].copy() + ), + "coarse_phase_increment": float( + coarse_phase_increments[ + start_index + ] + ), + } + + if best_result is None: + raise RuntimeError( + "Не удалось выполнить поиск радиокадра" + ) + + # -------------------------------------------------------- + # Уточнение CFO частотным поиском + # -------------------------------------------------------- + + coarse_frequency_hz = ( + best_result[ + "coarse_phase_increment" + ] + * SYMBOL_RATE + / (2.0 * np.pi) + ) + + frequency_candidates_hz = np.arange( + ( + coarse_frequency_hz + - CFO_REFINEMENT_HALF_WIDTH_HZ + ), + ( + coarse_frequency_hz + + CFO_REFINEMENT_HALF_WIDTH_HZ + + CFO_REFINEMENT_STEP_HZ / 2.0 + ), + CFO_REFINEMENT_STEP_HZ, + dtype=np.float64, + ) + + phase_increment_candidates = ( + 2.0 + * np.pi + * frequency_candidates_hz + / SYMBOL_RATE + ) + + candidate_compensation = np.exp( + -1j + * phase_increment_candidates[ + :, np.newaxis + ] + * marker_indexes[ + np.newaxis, : + ] + ) + + refined_coherent_sums = np.sum( + best_result[ + "despread_marker" + ][ + np.newaxis, : + ] + * candidate_compensation, + axis=1, + ) + + best_frequency_index = int( + np.argmax( + np.abs( + refined_coherent_sums + ) + ) + ) + + estimated_frequency_hz = float( + frequency_candidates_hz[ + best_frequency_index + ] + ) + + estimated_phase_increment = float( + phase_increment_candidates[ + best_frequency_index + ] + ) + + estimated_initial_phase = float( + np.angle( + refined_coherent_sums[ + best_frequency_index + ] + ) + ) + + selected_marker_energy = float( + np.sum( + np.abs( + best_result[ + "despread_marker" + ] + ) ** 2 + ) + ) + + refined_score = float( + np.abs( + refined_coherent_sums[ + best_frequency_index + ] + ) + / ( + np.sqrt( + selected_marker_energy + * marker_length + ) + + 1e-12 + ) + ) + + best_result.update( + { + "score": refined_score, + "frequency_offset_hz": ( + estimated_frequency_hz + ), + "phase_increment": ( + estimated_phase_increment + ), + "initial_phase": ( + estimated_initial_phase + ), + } + ) + + return best_result + + +# ============================================================ +# Разбор восстановленного радиокадра +# ============================================================ + +def decode_radio_frame( + corrected_symbols: np.ndarray, + frame_start_symbol: int, +) -> str: + """ + Демодулировать радиокадр и вернуть статус. + """ + + received_bits = bpsk_demodulate( + corrected_symbols + ) + + available_bits = received_bits[ + frame_start_symbol: + ] + + radio_header_start = ( + PREAMBLE_BIT_COUNT + ) + + radio_header_end = ( + radio_header_start + + 32 + ) + + if len(available_bits) < radio_header_end: + return STATUS_HEADER_ERROR + + try: + radio_header = bits_to_bytes( + available_bits[ + radio_header_start: + radio_header_end + ] + ) + + ( + received_radio_sync, + protocol_packet_length, + ) = struct.unpack( + ">HH", + radio_header, + ) + + except (ValueError, struct.error): + return STATUS_HEADER_ERROR + + if received_radio_sync != RADIO_SYNC_WORD: + return STATUS_HEADER_ERROR + + if not ( + 1 + <= protocol_packet_length + <= MAX_PROTOCOL_PACKET_SIZE + ): + return STATUS_HEADER_ERROR + + protocol_packet_start = ( + radio_header_end + ) + + protocol_packet_end = ( + protocol_packet_start + + protocol_packet_length * 8 + ) + + if len(available_bits) < protocol_packet_end: + return STATUS_HEADER_ERROR + + try: + protocol_packet = bits_to_bytes( + available_bits[ + protocol_packet_start: + protocol_packet_end + ] + ) + + parsed_packet = parse_packet( + protocol_packet + ) + + except CRCError: + return STATUS_CRC_ERROR + + except (PacketError, ValueError): + return STATUS_PACKET_ERROR + + try: + restored_message = ( + parsed_packet.payload.decode( + "utf-8" + ) + ) + + except UnicodeDecodeError: + return STATUS_PACKET_ERROR + + if ( + parsed_packet.message_type + != MESSAGE_TYPE_TEXT + or parsed_packet.sequence_number + != EXPECTED_SEQUENCE_NUMBER + or restored_message + != EXPECTED_MESSAGE + ): + return STATUS_PACKET_ERROR + + return STATUS_SUCCESS + + +# ============================================================ +# Загрузка IQ-сигнала +# ============================================================ + +if not INPUT_IQ_PATH.exists(): + raise FileNotFoundError( + f"Не найден IQ-файл: {INPUT_IQ_PATH}. " + "Сначала необходимо выполнить Lab018." + ) + +transmitted_iq = np.load( + INPUT_IQ_PATH +).astype( + np.complex128 +) + +if transmitted_iq.ndim != 1: + raise ValueError( + "IQ-массив должен быть одномерным" + ) + +if not np.iscomplexobj( + transmitted_iq +): + raise ValueError( + "IQ-массив должен быть комплексным" + ) + + +# ============================================================ +# Подготовка приёмника +# ============================================================ + +rrc_taps = root_raised_cosine_taps( + rolloff=RRC_ROLLOFF, + samples_per_symbol=SAMPLES_PER_SYMBOL, + span_symbols=RRC_SPAN_SYMBOLS, +) + +marker_bits, marker_symbols = ( + build_frame_marker() +) + +signal_energy_per_bit = ( + np.sum( + np.abs(transmitted_iq) ** 2 + ) + / RADIO_FRAME_BIT_COUNT +) + + +# ============================================================ +# Основной эксперимент +# ============================================================ + +results = [] + +for eb_n0_db in EB_N0_VALUES_DB: + + for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ + ): + + random_generator = ( + np.random.default_rng( + RANDOM_SEED + + int( + eb_n0_db * 100 + ) + + int( + max_abs_cfo_hz + ) + ) + ) + + success_without_cfo = 0 + success_with_cfo = 0 + + detection_fail_count = 0 + + correlation_scores = [] + frequency_errors_hz = [] + + for trial_index in range( + TRIALS_PER_POINT + ): + + if max_abs_cfo_hz == 0.0: + true_frequency_hz = 0.0 + + else: + true_frequency_hz = float( + random_generator.uniform( + -max_abs_cfo_hz, + max_abs_cfo_hz, + ) + ) + + phase_offset_radians = float( + random_generator.uniform( + -np.pi, + np.pi, + ) + ) + + sample_delay = int( + random_generator.integers( + 0, + MAX_RANDOM_DELAY_SAMPLES + 1, + ) + ) + + impaired_iq = apply_frequency_offset( + iq_samples=transmitted_iq, + frequency_offset_hz=( + true_frequency_hz + ), + phase_offset_radians=( + phase_offset_radians + ), + ) + + clean_received_iq = np.concatenate( + [ + np.zeros( + sample_delay, + dtype=np.complex128, + ), + impaired_iq, + np.zeros( + 2 * SAMPLES_PER_SYMBOL, + dtype=np.complex128, + ), + ] + ) + + noisy_received_iq = add_awgn_for_eb_n0( + clean_iq=clean_received_iq, + signal_energy_per_bit=( + signal_energy_per_bit + ), + eb_n0_db=eb_n0_db, + random_generator=( + random_generator + ), + ) + + matched_iq = fftconvolve( + noisy_received_iq, + rrc_taps, + mode="full", + ) + + search_result = ( + find_frame_and_frequency_offset( + matched_iq=matched_iq, + marker_symbols=marker_symbols, + ) + ) + + correlation_score = ( + search_result["score"] + ) + + correlation_scores.append( + correlation_score + ) + + if ( + correlation_score + < DETECTION_THRESHOLD + ): + detection_fail_count += 1 + continue + + symbol_samples = search_result[ + "symbol_samples" + ] + + frame_start_symbol = search_result[ + "start_symbol_index" + ] + + estimated_frequency_hz = ( + search_result[ + "frequency_offset_hz" + ] + ) + + frequency_errors_hz.append( + estimated_frequency_hz + - true_frequency_hz + ) + + estimated_initial_phase = ( + search_result[ + "initial_phase" + ] + ) + + estimated_phase_increment = ( + search_result[ + "phase_increment" + ] + ) + + symbol_indexes = np.arange( + len(symbol_samples), + dtype=np.float64, + ) + + relative_symbol_indexes = ( + symbol_indexes + - frame_start_symbol + ) + + # ----------------------------------------------- + # Только постоянная фазовая коррекция + # ----------------------------------------------- + + constant_phase_corrected = ( + symbol_samples + * np.exp( + -1j + * estimated_initial_phase + ) + ) + + status_without = decode_radio_frame( + corrected_symbols=( + constant_phase_corrected + ), + frame_start_symbol=( + frame_start_symbol + ), + ) + + if status_without == STATUS_SUCCESS: + success_without_cfo += 1 + + # ----------------------------------------------- + # Фазовая и частотная коррекция + # ----------------------------------------------- + + complete_phase_model = ( + estimated_initial_phase + + estimated_phase_increment + * relative_symbol_indexes + ) + + frequency_corrected = ( + symbol_samples + * np.exp( + -1j + * complete_phase_model + ) + ) + + status_with = decode_radio_frame( + corrected_symbols=( + frequency_corrected + ), + frame_start_symbol=( + frame_start_symbol + ), + ) + + if status_with == STATUS_SUCCESS: + success_with_cfo += 1 + + fer_without_cfo = ( + 1.0 + - success_without_cfo + / TRIALS_PER_POINT + ) + + fer_with_cfo = ( + 1.0 + - success_with_cfo + / TRIALS_PER_POINT + ) + + mean_correlation = float( + np.mean( + correlation_scores + ) + ) + + if frequency_errors_hz: + + frequency_errors_array = np.array( + frequency_errors_hz, + dtype=np.float64, + ) + + mean_abs_frequency_error_hz = float( + np.mean( + np.abs( + frequency_errors_array + ) + ) + ) + + frequency_rmse_hz = float( + np.sqrt( + np.mean( + frequency_errors_array ** 2 + ) + ) + ) + + else: + mean_abs_frequency_error_hz = float( + "nan" + ) + + frequency_rmse_hz = float( + "nan" + ) + + results.append( + { + "eb_n0_db": eb_n0_db, + "max_abs_cfo_hz": ( + max_abs_cfo_hz + ), + "trials": TRIALS_PER_POINT, + "success_without_cfo": ( + success_without_cfo + ), + "success_with_cfo": ( + success_with_cfo + ), + "detection_fail_count": ( + detection_fail_count + ), + "fer_without_cfo": ( + fer_without_cfo + ), + "fer_with_cfo": ( + fer_with_cfo + ), + "mean_correlation": ( + mean_correlation + ), + "mean_abs_frequency_error_hz": ( + mean_abs_frequency_error_hz + ), + "frequency_rmse_hz": ( + frequency_rmse_hz + ), + } + ) + + +# ============================================================ +# Вывод таблицы +# ============================================================ + +print( + "=== Lab022. Шум и частотное рассогласование ===" +) + +print("\nПопыток на каждую точку:") + +print(TRIALS_PER_POINT) + +print("\nРезультаты:") + +print( + f"{'Eb/N0':>9}" + f"{'CFO ±':>10}" + f"{'Успех без':>12}" + f"{'Успех с':>11}" + f"{'FER без':>10}" + f"{'FER с':>10}" + f"{'Нет кадра':>11}" + f"{'CFO RMSE':>12}" + f"{'Коррел.':>10}" +) + +print("-" * 95) + +for result in results: + + print( + f"{result['eb_n0_db']:>6.1f} дБ" + f"{result['max_abs_cfo_hz']:>7.0f} Гц" + f"{result['success_without_cfo']:>12}" + f"{result['success_with_cfo']:>11}" + f"{result['fer_without_cfo']:>10.3f}" + f"{result['fer_with_cfo']:>10.3f}" + f"{result['detection_fail_count']:>11}" + f"{result['frequency_rmse_hz']:>9.2f} Гц" + f"{result['mean_correlation']:>10.3f}" + ) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + writer = DictWriter( + csv_file, + fieldnames=list( + results[0].keys() + ), + ) + + writer.writeheader() + writer.writerows(results) + + +# ============================================================ +# Графики +# ============================================================ + +figure, axes = plt.subplots( + 2, + 2, + figsize=(13, 10), +) + + +# ------------------------------------------------------------ +# 1. FER без CFO-компенсации +# ------------------------------------------------------------ + +measurement_floor = ( + 0.5 + / TRIALS_PER_POINT +) + +for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ +): + + selected_results = [ + result + for result in results + if ( + result["max_abs_cfo_hz"] + == max_abs_cfo_hz + ) + ] + + eb_values = [ + result["eb_n0_db"] + for result in selected_results + ] + + fer_values = [ + max( + result["fer_without_cfo"], + measurement_floor, + ) + for result in selected_results + ] + + axes[0, 0].semilogy( + eb_values, + fer_values, + marker="o", + label=( + f"CFO ±{max_abs_cfo_hz:.0f} Гц" + ), + ) + +axes[0, 0].set_xlabel( + "Eb/N0, дБ" +) + +axes[0, 0].set_ylabel( + "FER" +) + +axes[0, 0].set_title( + "FER без компенсации CFO" +) + +axes[0, 0].grid( + True, + which="both", +) + +axes[0, 0].legend() + + +# ------------------------------------------------------------ +# 2. FER после CFO-компенсации +# ------------------------------------------------------------ + +for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ +): + + selected_results = [ + result + for result in results + if ( + result["max_abs_cfo_hz"] + == max_abs_cfo_hz + ) + ] + + eb_values = [ + result["eb_n0_db"] + for result in selected_results + ] + + fer_values = [ + max( + result["fer_with_cfo"], + measurement_floor, + ) + for result in selected_results + ] + + axes[0, 1].semilogy( + eb_values, + fer_values, + marker="s", + label=( + f"CFO ±{max_abs_cfo_hz:.0f} Гц" + ), + ) + +axes[0, 1].set_xlabel( + "Eb/N0, дБ" +) + +axes[0, 1].set_ylabel( + "FER" +) + +axes[0, 1].set_title( + "FER после компенсации CFO" +) + +axes[0, 1].grid( + True, + which="both", +) + +axes[0, 1].legend() + + +# ------------------------------------------------------------ +# 3. Ошибка оценки CFO +# ------------------------------------------------------------ + +for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ +): + + selected_results = [ + result + for result in results + if ( + result["max_abs_cfo_hz"] + == max_abs_cfo_hz + ) + ] + + axes[1, 0].plot( + [ + result["eb_n0_db"] + for result in selected_results + ], + [ + result["frequency_rmse_hz"] + for result in selected_results + ], + marker="o", + label=( + f"CFO ±{max_abs_cfo_hz:.0f} Гц" + ), + ) + +axes[1, 0].set_xlabel( + "Eb/N0, дБ" +) + +axes[1, 0].set_ylabel( + "CFO RMSE, Гц" +) + +axes[1, 0].set_title( + "Среднеквадратичная ошибка оценки CFO" +) + +axes[1, 0].grid( + True +) + +axes[1, 0].legend() + + +# ------------------------------------------------------------ +# 4. Корреляционная оценка +# ------------------------------------------------------------ + +for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ +): + + selected_results = [ + result + for result in results + if ( + result["max_abs_cfo_hz"] + == max_abs_cfo_hz + ) + ] + + axes[1, 1].plot( + [ + result["eb_n0_db"] + for result in selected_results + ], + [ + result["mean_correlation"] + for result in selected_results + ], + marker="o", + label=( + f"CFO ±{max_abs_cfo_hz:.0f} Гц" + ), + ) + +axes[1, 1].axhline( + DETECTION_THRESHOLD, + linestyle=":", + label="Порог обнаружения", +) + +axes[1, 1].set_xlabel( + "Eb/N0, дБ" +) + +axes[1, 1].set_ylabel( + "Средняя корреляция" +) + +axes[1, 1].set_title( + "Качество обнаружения маркера" +) + +axes[1, 1].grid( + True +) + +axes[1, 1].legend() + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Текстовый отчёт +# ============================================================ + +report_lines = [ + "Lab022. AWGN and carrier frequency offset", + "", + ( + "Trials per point: " + f"{TRIALS_PER_POINT}" + ), + ( + "Detection threshold: " + f"{DETECTION_THRESHOLD:.3f}" + ), + "", +] + +for result in results: + + report_lines.extend( + [ + ( + "Eb/N0: " + f"{result['eb_n0_db']:.1f} dB" + ), + ( + "Maximum absolute CFO: " + f"{result['max_abs_cfo_hz']:.1f} Hz" + ), + ( + "Success without CFO correction: " + f"{result['success_without_cfo']}" + ), + ( + "Success with CFO correction: " + f"{result['success_with_cfo']}" + ), + ( + "FER without correction: " + f"{result['fer_without_cfo']:.6f}" + ), + ( + "FER with correction: " + f"{result['fer_with_cfo']:.6f}" + ), + ( + "CFO RMSE: " + f"{result['frequency_rmse_hz']:.3f} Hz" + ), + "", + ] + ) + +REPORT_PATH.write_text( + "\n".join( + report_lines + ), + encoding="utf-8", +) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +result_10_db_no_cfo = next( + result + for result in results + if ( + result["eb_n0_db"] == 10.0 + and result["max_abs_cfo_hz"] == 0.0 + ) +) + +result_10_db_large_cfo = next( + result + for result in results + if ( + result["eb_n0_db"] == 10.0 + and result["max_abs_cfo_hz"] == 1000.0 + ) +) + +total_success_without_large_cfo = sum( + result["success_without_cfo"] + for result in results + if result["max_abs_cfo_hz"] == 1000.0 +) + +total_success_with_large_cfo = sum( + result["success_with_cfo"] + for result in results + if result["max_abs_cfo_hz"] == 1000.0 +) + +assert len(results) == ( + len(EB_N0_VALUES_DB) + * len(MAX_ABS_CFO_VALUES_HZ) +) + +assert ( + result_10_db_no_cfo["success_with_cfo"] + >= 25 +) + +assert ( + result_10_db_large_cfo["success_with_cfo"] + >= 24 +) + +assert ( + total_success_with_large_cfo + > total_success_without_large_cfo +) + +assert CSV_PATH.exists() +assert GRAPH_PATH.exists() +assert REPORT_PATH.exists() + + +print("\nCSV:") + +print(CSV_PATH) + +print("\nГрафик:") + +print(GRAPH_PATH) + +print("\nОтчёт:") + +print(REPORT_PATH) + +print( + "\nПроверка пройдена: " + "совместное действие шума и CFO исследовано." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab023_guarded_cfo_correction.py b/Projects/SDR_Rover/tests/lab023_guarded_cfo_correction.py new file mode 100644 index 0000000..d895e0b --- /dev/null +++ b/Projects/SDR_Rover/tests/lab023_guarded_cfo_correction.py @@ -0,0 +1,1637 @@ +""" +Lab023. Защищённая компенсация частотной ошибки. + +Сравниваются три стратегии: + +1. PHASE_ONLY + Компенсируется только постоянный фазовый поворот. + +2. ALWAYS_CFO + Оценённое частотное рассогласование применяется всегда. + +3. GUARDED_CFO + Частотная коррекция применяется только тогда, когда: + + - модуль оценённого CFO превышает мёртвую зону; + - межсимвольная фазовая оценка достаточно достоверна. + +Эксперимент выполняется на полном 320-битном радиокадре: + + PREAMBLE + + RADIO SYNC + + LENGTH + + SDR Rover Link packet + +В каждой попытке добавляются: + +- случайная начальная фаза; +- случайный CFO; +- AWGN-шум. + +Символьная синхронизация в этой лабораторной считается +уже выполненной. Это позволяет исследовать именно логику +управления CFO-компенсацией. +""" + +from csv import DictWriter +from pathlib import Path +import struct + +import matplotlib.pyplot as plt +import numpy as np + +from protocol.packet import ( + CRCError, + MESSAGE_TYPE_TEXT, + PacketError, + build_packet, + parse_packet, +) + + +# ============================================================ +# Параметры радиокадра +# ============================================================ + +RADIO_SYNC_WORD = 0xD391 + +PREAMBLE_BIT_COUNT = 64 + +SYMBOL_RATE = 20_000 + +EXPECTED_MESSAGE = "ПРИВЕТ SDR" +EXPECTED_SEQUENCE_NUMBER = 18 + + +# ============================================================ +# Параметры защищённой CFO-коррекции +# ============================================================ + +# Если модуль оценки меньше этого значения, +# приёмник считает CFO практически нулевым. +CFO_DEAD_ZONE_HZ = 15.0 + +# Достоверность фазового приращения: +# +# 0.0 — полностью случайная фаза; +# 1.0 — идеально постоянное межсимвольное вращение. +MINIMUM_PHASE_CONSISTENCY = 0.55 +# Уточняющий поиск около грубой оценки CFO. +# +# Грубая оценка даёт приблизительный центр, +# после чего перебираются частоты вокруг него. +CFO_REFINEMENT_HALF_WIDTH_HZ = 200.0 + +CFO_REFINEMENT_STEP_HZ = 1.0 + +# ============================================================ +# Параметры эксперимента +# ============================================================ + +EB_N0_VALUES_DB = [ + 6.0, + 7.0, + 8.0, + 10.0, +] + +# В каждой попытке CFO выбирается случайно: +# +# -MAX_CFO ... +MAX_CFO +MAX_ABS_CFO_VALUES_HZ = [ + 0.0, + 250.0, + 1000.0, +] + +TRIALS_PER_POINT = 200 + +RANDOM_SEED = 2026 + + +# ============================================================ +# Выходные файлы +# ============================================================ + +OUTPUT_DIRECTORY = Path( + "data/processed/lab023" +) + +OUTPUT_DIRECTORY.mkdir( + parents=True, + exist_ok=True, +) + +CSV_PATH = ( + OUTPUT_DIRECTORY + / "lab023_guarded_cfo_results.csv" +) + +GRAPH_PATH = ( + OUTPUT_DIRECTORY + / "lab023_guarded_cfo.png" +) + +REPORT_PATH = ( + OUTPUT_DIRECTORY + / "lab023_guarded_cfo_report.txt" +) + + +# ============================================================ +# Статусы декодирования +# ============================================================ + +STATUS_SUCCESS = "SUCCESS" +STATUS_HEADER_ERROR = "HEADER ERROR" +STATUS_CRC_ERROR = "CRC ERROR" +STATUS_PACKET_ERROR = "PACKET ERROR" + + +# ============================================================ +# Преобразование bytes и bits +# ============================================================ + +def bytes_to_bits( + data: bytes, +) -> np.ndarray: + """ + Преобразовать bytes в одномерный массив битов. + """ + + if not isinstance(data, bytes): + raise TypeError( + "data должен иметь тип bytes" + ) + + return np.unpackbits( + np.frombuffer( + data, + dtype=np.uint8, + ) + ) + + +def bits_to_bytes( + bits: np.ndarray, +) -> bytes: + """ + Упаковать массив битов обратно в bytes. + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + if bits.ndim != 1: + raise ValueError( + "bits должен быть одномерным массивом" + ) + + if len(bits) % 8 != 0: + raise ValueError( + "Количество битов должно быть кратно восьми" + ) + + if not np.all( + (bits == 0) | (bits == 1) + ): + raise ValueError( + "bits должен содержать только 0 и 1" + ) + + return np.packbits( + bits + ).tobytes() + + +# ============================================================ +# BPSK +# ============================================================ + +def bpsk_modulate( + bits: np.ndarray, +) -> np.ndarray: + """ + Преобразовать биты в BPSK-символы. + + 0 → -1 + 1 → +1 + """ + + bits = np.asarray( + bits, + dtype=np.uint8, + ) + + symbols = ( + 2.0 + * bits.astype(np.float64) + - 1.0 + ) + + return symbols.astype( + np.complex128 + ) + + +def bpsk_demodulate( + symbols: np.ndarray, +) -> np.ndarray: + """ + Демодулировать BPSK по знаку компоненты I. + """ + + return ( + symbols.real >= 0.0 + ).astype(np.uint8) + + +# ============================================================ +# Формирование полного радиокадра +# ============================================================ + +def build_radio_frame( +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Сформировать: + + - биты полного радиокадра; + - BPSK-символы полного радиокадра; + - известные символы PREAMBLE + RADIO SYNC. + """ + + payload = EXPECTED_MESSAGE.encode( + "utf-8" + ) + + protocol_packet = build_packet( + payload=payload, + message_type=MESSAGE_TYPE_TEXT, + sequence_number=EXPECTED_SEQUENCE_NUMBER, + ) + + if len(protocol_packet) > 65535: + raise ValueError( + "Внутренний пакет слишком велик" + ) + + radio_header = struct.pack( + ">HH", + RADIO_SYNC_WORD, + len(protocol_packet), + ) + + preamble_bits = np.tile( + np.array( + [1, 0], + dtype=np.uint8, + ), + PREAMBLE_BIT_COUNT // 2, + ) + + radio_header_bits = bytes_to_bits( + radio_header + ) + + protocol_packet_bits = bytes_to_bits( + protocol_packet + ) + + frame_bits = np.concatenate( + [ + preamble_bits, + radio_header_bits, + protocol_packet_bits, + ] + ) + + frame_symbols = bpsk_modulate( + frame_bits + ) + + marker_bit_count = ( + PREAMBLE_BIT_COUNT + + 16 + ) + + marker_symbols = frame_symbols[ + :marker_bit_count + ] + + return ( + frame_bits, + frame_symbols, + marker_symbols, + ) + + +# ============================================================ +# Искажения канала +# ============================================================ + +def apply_carrier_impairments( + symbols: np.ndarray, + frequency_offset_hz: float, + initial_phase_radians: float, +) -> np.ndarray: + """ + Добавить начальный фазовый поворот и CFO. + + Между соседними символами фаза изменяется на: + + 2π · CFO / SYMBOL_RATE + """ + + symbol_indexes = np.arange( + len(symbols), + dtype=np.float64, + ) + + phase_increment = ( + 2.0 + * np.pi + * frequency_offset_hz + / SYMBOL_RATE + ) + + phase_values = ( + initial_phase_radians + + phase_increment + * symbol_indexes + ) + + return ( + symbols + * np.exp( + 1j * phase_values + ) + ) + + +def add_awgn( + symbols: np.ndarray, + eb_n0_db: float, + random_generator: np.random.Generator, +) -> np.ndarray: + """ + Добавить комплексный AWGN. + + Энергия одного BPSK-символа равна единице. + Один символ переносит один бит. + """ + + eb_n0_linear = 10.0 ** ( + eb_n0_db / 10.0 + ) + + component_sigma = np.sqrt( + 1.0 + / ( + 2.0 + * eb_n0_linear + ) + ) + + noise = component_sigma * ( + random_generator.standard_normal( + len(symbols) + ) + + 1j + * random_generator.standard_normal( + len(symbols) + ) + ) + + return symbols + noise + + +# ============================================================ +# Оценка фазы и CFO по известному маркеру +# ============================================================ + +def estimate_carrier_parameters( + received_symbols: np.ndarray, + marker_symbols: np.ndarray, +) -> dict: + """ + Оценить фазу и частотное рассогласование по маркеру. + + Алгоритм: + + 1. Удалить известные BPSK-знаки маркера. + 2. Получить грубую оценку CFO по соседним символам. + 3. Выполнить уточняющий частотный поиск. + 4. Оценить начальную фазу после компенсации CFO. + 5. Рассчитать достоверность оценки. + """ + + marker_length = len( + marker_symbols + ) + + received_marker = received_symbols[ + :marker_length + ] + + if len(received_marker) != marker_length: + raise ValueError( + "Недостаточно символов маркера" + ) + + # Известные BPSK-знаки равны -1 или +1. + # Умножение удаляет переданную манипуляцию, + # оставляя фазу канала и шум. + despread_marker = ( + received_marker + * marker_symbols + ) + + marker_indexes = np.arange( + marker_length, + dtype=np.float64, + ) + + marker_magnitude_sum = float( + np.sum( + np.abs( + despread_marker + ) + ) + ) + + # ======================================================== + # Постоянная фаза без CFO-компенсации + # ======================================================== + + constant_coherent_sum = np.sum( + despread_marker + ) + + constant_phase = float( + np.angle( + constant_coherent_sum + ) + ) + + constant_coherence = float( + np.abs( + constant_coherent_sum + ) + / ( + marker_magnitude_sum + + 1e-12 + ) + ) + + # ======================================================== + # Грубая оценка CFO + # ======================================================== + + adjacent_products = ( + despread_marker[1:] + * np.conj( + despread_marker[:-1] + ) + ) + + adjacent_sum = np.sum( + adjacent_products + ) + + coarse_phase_increment = float( + np.angle( + adjacent_sum + ) + ) + + coarse_frequency_hz = ( + coarse_phase_increment + * SYMBOL_RATE + / (2.0 * np.pi) + ) + + phase_consistency = float( + np.abs( + adjacent_sum + ) + / ( + np.sum( + np.abs( + adjacent_products + ) + ) + + 1e-12 + ) + ) + + # ======================================================== + # Уточняющий поиск CFO + # ======================================================== + + frequency_candidates_hz = np.arange( + ( + coarse_frequency_hz + - CFO_REFINEMENT_HALF_WIDTH_HZ + ), + ( + coarse_frequency_hz + + CFO_REFINEMENT_HALF_WIDTH_HZ + + CFO_REFINEMENT_STEP_HZ / 2.0 + ), + CFO_REFINEMENT_STEP_HZ, + dtype=np.float64, + ) + + phase_increment_candidates = ( + 2.0 + * np.pi + * frequency_candidates_hz + / SYMBOL_RATE + ) + + candidate_compensation = np.exp( + -1j + * phase_increment_candidates[ + :, np.newaxis + ] + * marker_indexes[ + np.newaxis, : + ] + ) + + coherent_sums = np.sum( + despread_marker[ + np.newaxis, : + ] + * candidate_compensation, + axis=1, + ) + + best_candidate_index = int( + np.argmax( + np.abs( + coherent_sums + ) + ) + ) + + estimated_frequency_hz = float( + frequency_candidates_hz[ + best_candidate_index + ] + ) + + phase_increment = float( + phase_increment_candidates[ + best_candidate_index + ] + ) + + best_coherent_sum = ( + coherent_sums[ + best_candidate_index + ] + ) + + initial_phase_after_cfo = float( + np.angle( + best_coherent_sum + ) + ) + + cfo_coherence = float( + np.abs( + best_coherent_sum + ) + / ( + marker_magnitude_sum + + 1e-12 + ) + ) + + coherence_gain = ( + cfo_coherence + - constant_coherence + ) + + return { + "constant_phase": constant_phase, + "constant_coherence": constant_coherence, + "phase_increment": phase_increment, + "estimated_frequency_hz": ( + estimated_frequency_hz + ), + "initial_phase_after_cfo": ( + initial_phase_after_cfo + ), + "phase_consistency": phase_consistency, + "cfo_coherence": cfo_coherence, + "coherence_gain": coherence_gain, + } + +# ============================================================ +# Коррекция несущей +# ============================================================ + +def correct_constant_phase( + received_symbols: np.ndarray, + phase_radians: float, +) -> np.ndarray: + """ + Компенсировать только постоянную фазу. + """ + + return ( + received_symbols + * np.exp( + -1j * phase_radians + ) + ) + + +def correct_phase_and_frequency( + received_symbols: np.ndarray, + initial_phase_radians: float, + phase_increment: float, +) -> np.ndarray: + """ + Компенсировать постоянную фазу и CFO. + """ + + symbol_indexes = np.arange( + len(received_symbols), + dtype=np.float64, + ) + + phase_model = ( + initial_phase_radians + + phase_increment + * symbol_indexes + ) + + return ( + received_symbols + * np.exp( + -1j * phase_model + ) + ) + + +def should_apply_cfo_correction( + estimated_frequency_hz: float, + phase_consistency: float, + coherence_gain: float, +) -> bool: + """ + Разрешить CFO-коррекцию только при наличии + достаточных оснований. + + Требования: + + 1. Оценка находится вне мёртвой зоны. + 2. Межсимвольное вращение достаточно согласованно. + 3. Компенсация CFO действительно повышает + когерентность известного маркера. + """ + + MINIMUM_COHERENCE_GAIN = 0.02 + + return ( + abs( + estimated_frequency_hz + ) + >= CFO_DEAD_ZONE_HZ + and phase_consistency + >= MINIMUM_PHASE_CONSISTENCY + and coherence_gain + >= MINIMUM_COHERENCE_GAIN + ) + + +# ============================================================ +# Разбор радиокадра +# ============================================================ + +def decode_radio_frame( + corrected_symbols: np.ndarray, +) -> str: + """ + Демодулировать полный радиокадр. + + Возвращает статус приёма. + """ + + received_bits = bpsk_demodulate( + corrected_symbols + ) + + radio_header_start = ( + PREAMBLE_BIT_COUNT + ) + + radio_header_end = ( + radio_header_start + + 32 + ) + + if len(received_bits) < radio_header_end: + return STATUS_HEADER_ERROR + + try: + radio_header = bits_to_bytes( + received_bits[ + radio_header_start: + radio_header_end + ] + ) + + ( + received_sync, + protocol_packet_length, + ) = struct.unpack( + ">HH", + radio_header, + ) + + except (ValueError, struct.error): + return STATUS_HEADER_ERROR + + if received_sync != RADIO_SYNC_WORD: + return STATUS_HEADER_ERROR + + if not 1 <= protocol_packet_length <= 4096: + return STATUS_HEADER_ERROR + + protocol_packet_start = ( + radio_header_end + ) + + protocol_packet_end = ( + protocol_packet_start + + protocol_packet_length * 8 + ) + + if len(received_bits) < protocol_packet_end: + return STATUS_HEADER_ERROR + + try: + protocol_packet = bits_to_bytes( + received_bits[ + protocol_packet_start: + protocol_packet_end + ] + ) + + parsed_packet = parse_packet( + protocol_packet + ) + + except CRCError: + return STATUS_CRC_ERROR + + except (PacketError, ValueError): + return STATUS_PACKET_ERROR + + try: + restored_message = ( + parsed_packet.payload.decode( + "utf-8" + ) + ) + + except UnicodeDecodeError: + return STATUS_PACKET_ERROR + + if ( + parsed_packet.message_type + != MESSAGE_TYPE_TEXT + or parsed_packet.sequence_number + != EXPECTED_SEQUENCE_NUMBER + or restored_message + != EXPECTED_MESSAGE + ): + return STATUS_PACKET_ERROR + + return STATUS_SUCCESS + + +# ============================================================ +# Подготовка исходного кадра +# ============================================================ + +( + transmitted_frame_bits, + transmitted_symbols, + marker_symbols, +) = build_radio_frame() + +print( + "=== Lab023. Защищённая CFO-компенсация ===" +) + +print("\nРазмер полного радиокадра:") + +print( + len(transmitted_frame_bits), + "битов" +) + +print("\nМёртвая зона CFO:") + +print( + f"±{CFO_DEAD_ZONE_HZ:.1f}", + "Гц" +) + +print("\nМинимальная достоверность фазовой оценки:") + +print( + f"{MINIMUM_PHASE_CONSISTENCY:.2f}" +) + + +# ============================================================ +# Основной эксперимент +# ============================================================ + +results = [] + +for eb_n0_db in EB_N0_VALUES_DB: + + for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ + ): + + seed = ( + RANDOM_SEED + + int( + eb_n0_db * 1000 + ) + + int( + max_abs_cfo_hz + ) + ) + + random_generator = np.random.default_rng( + seed + ) + + success_phase_only = 0 + success_always_cfo = 0 + success_guarded_cfo = 0 + + guarded_apply_count = 0 + + frequency_errors = [] + phase_consistency_values = [] + + for trial_index in range( + TRIALS_PER_POINT + ): + + if max_abs_cfo_hz == 0.0: + true_frequency_hz = 0.0 + + else: + true_frequency_hz = float( + random_generator.uniform( + -max_abs_cfo_hz, + max_abs_cfo_hz, + ) + ) + + initial_phase = float( + random_generator.uniform( + -np.pi, + np.pi, + ) + ) + + impaired_symbols = ( + apply_carrier_impairments( + symbols=transmitted_symbols, + frequency_offset_hz=( + true_frequency_hz + ), + initial_phase_radians=( + initial_phase + ), + ) + ) + + received_symbols = add_awgn( + symbols=impaired_symbols, + eb_n0_db=eb_n0_db, + random_generator=random_generator, + ) + + estimate = estimate_carrier_parameters( + received_symbols=received_symbols, + marker_symbols=marker_symbols, + ) + + estimated_frequency_hz = ( + estimate[ + "estimated_frequency_hz" + ] + ) + + phase_consistency = ( + estimate[ + "phase_consistency" + ] + ) + + frequency_errors.append( + estimated_frequency_hz + - true_frequency_hz + ) + + phase_consistency_values.append( + phase_consistency + ) + + # ================================================= + # Стратегия 1. Только постоянная фаза + # ================================================= + + phase_only_symbols = ( + correct_constant_phase( + received_symbols=( + received_symbols + ), + phase_radians=( + estimate[ + "constant_phase" + ] + ), + ) + ) + + status_phase_only = ( + decode_radio_frame( + phase_only_symbols + ) + ) + + if ( + status_phase_only + == STATUS_SUCCESS + ): + success_phase_only += 1 + + # ================================================= + # Стратегия 2. CFO применяется всегда + # ================================================= + + always_cfo_symbols = ( + correct_phase_and_frequency( + received_symbols=( + received_symbols + ), + initial_phase_radians=( + estimate[ + "initial_phase_after_cfo" + ] + ), + phase_increment=( + estimate[ + "phase_increment" + ] + ), + ) + ) + + status_always_cfo = ( + decode_radio_frame( + always_cfo_symbols + ) + ) + + if ( + status_always_cfo + == STATUS_SUCCESS + ): + success_always_cfo += 1 + + # ================================================= + # Стратегия 3. Защищённая CFO-коррекция + # ================================================= + + apply_cfo = ( + should_apply_cfo_correction( + estimated_frequency_hz=( + estimated_frequency_hz + ), + phase_consistency=( + phase_consistency + ), + coherence_gain=( + estimate[ + "coherence_gain" + ] + ), + ) + ) + + if apply_cfo: + guarded_apply_count += 1 + + guarded_symbols = ( + always_cfo_symbols + ) + + else: + guarded_symbols = ( + phase_only_symbols + ) + + status_guarded_cfo = ( + decode_radio_frame( + guarded_symbols + ) + ) + + if ( + status_guarded_cfo + == STATUS_SUCCESS + ): + success_guarded_cfo += 1 + + # ===================================================== + # Итог одной экспериментальной точки + # ===================================================== + + frequency_errors_array = np.asarray( + frequency_errors, + dtype=np.float64, + ) + + frequency_rmse_hz = float( + np.sqrt( + np.mean( + frequency_errors_array ** 2 + ) + ) + ) + + mean_phase_consistency = float( + np.mean( + phase_consistency_values + ) + ) + + fer_phase_only = ( + 1.0 + - success_phase_only + / TRIALS_PER_POINT + ) + + fer_always_cfo = ( + 1.0 + - success_always_cfo + / TRIALS_PER_POINT + ) + + fer_guarded_cfo = ( + 1.0 + - success_guarded_cfo + / TRIALS_PER_POINT + ) + + guarded_apply_rate = ( + guarded_apply_count + / TRIALS_PER_POINT + ) + + results.append( + { + "eb_n0_db": eb_n0_db, + "max_abs_cfo_hz": ( + max_abs_cfo_hz + ), + "trials": TRIALS_PER_POINT, + "success_phase_only": ( + success_phase_only + ), + "success_always_cfo": ( + success_always_cfo + ), + "success_guarded_cfo": ( + success_guarded_cfo + ), + "fer_phase_only": ( + fer_phase_only + ), + "fer_always_cfo": ( + fer_always_cfo + ), + "fer_guarded_cfo": ( + fer_guarded_cfo + ), + "guarded_apply_count": ( + guarded_apply_count + ), + "guarded_apply_rate": ( + guarded_apply_rate + ), + "frequency_rmse_hz": ( + frequency_rmse_hz + ), + "mean_phase_consistency": ( + mean_phase_consistency + ), + } + ) + + +# ============================================================ +# Вывод результатов +# ============================================================ + +print("\nПопыток на каждую точку:") + +print(TRIALS_PER_POINT) + +print("\nРезультаты:") + +print( + f"{'Eb/N0':>9}" + f"{'CFO ±':>10}" + f"{'Усп. фаза':>12}" + f"{'Усп. всегда':>14}" + f"{'Усп. guard':>13}" + f"{'FER фаза':>11}" + f"{'FER всегда':>12}" + f"{'FER guard':>11}" + f"{'Guard on':>10}" + f"{'CFO RMSE':>11}" +) + +print("-" * 114) + +for result in results: + + print( + f"{result['eb_n0_db']:>6.1f} дБ" + f"{result['max_abs_cfo_hz']:>7.0f} Гц" + f"{result['success_phase_only']:>12}" + f"{result['success_always_cfo']:>14}" + f"{result['success_guarded_cfo']:>13}" + f"{result['fer_phase_only']:>11.3f}" + f"{result['fer_always_cfo']:>12.3f}" + f"{result['fer_guarded_cfo']:>11.3f}" + f"{result['guarded_apply_rate'] * 100:>8.1f} %" + f"{result['frequency_rmse_hz']:>8.2f} Гц" + ) + + +# ============================================================ +# Сохранение CSV +# ============================================================ + +with CSV_PATH.open( + "w", + newline="", + encoding="utf-8-sig", +) as csv_file: + + writer = DictWriter( + csv_file, + fieldnames=list( + results[0].keys() + ), + ) + + writer.writeheader() + writer.writerows(results) + + +# ============================================================ +# Подготовка графиков +# ============================================================ + +measurement_floor = ( + 0.5 + / TRIALS_PER_POINT +) + +figure, axes = plt.subplots( + 2, + 2, + figsize=(13, 10), +) + + +# ------------------------------------------------------------ +# 1. FER при истинном CFO = 0 +# ------------------------------------------------------------ + +zero_cfo_results = [ + result + for result in results + if result["max_abs_cfo_hz"] == 0.0 +] + +zero_eb_values = [ + result["eb_n0_db"] + for result in zero_cfo_results +] + +axes[0, 0].semilogy( + zero_eb_values, + [ + max( + result["fer_phase_only"], + measurement_floor, + ) + for result in zero_cfo_results + ], + marker="o", + label="Только постоянная фаза", +) + +axes[0, 0].semilogy( + zero_eb_values, + [ + max( + result["fer_always_cfo"], + measurement_floor, + ) + for result in zero_cfo_results + ], + marker="s", + label="CFO применяется всегда", +) + +axes[0, 0].semilogy( + zero_eb_values, + [ + max( + result["fer_guarded_cfo"], + measurement_floor, + ) + for result in zero_cfo_results + ], + marker="^", + label="Защищённая CFO-коррекция", +) + +axes[0, 0].set_xlabel( + "Eb/N0, дБ" +) + +axes[0, 0].set_ylabel( + "FER" +) + +axes[0, 0].set_title( + "Истинный CFO = 0 Гц" +) + +axes[0, 0].grid( + True, + which="both", +) + +axes[0, 0].legend() + + +# ------------------------------------------------------------ +# 2. FER при CFO ±1000 Гц +# ------------------------------------------------------------ + +large_cfo_results = [ + result + for result in results + if result["max_abs_cfo_hz"] == 1000.0 +] + +large_eb_values = [ + result["eb_n0_db"] + for result in large_cfo_results +] + +axes[0, 1].semilogy( + large_eb_values, + [ + max( + result["fer_phase_only"], + measurement_floor, + ) + for result in large_cfo_results + ], + marker="o", + label="Только постоянная фаза", +) + +axes[0, 1].semilogy( + large_eb_values, + [ + max( + result["fer_always_cfo"], + measurement_floor, + ) + for result in large_cfo_results + ], + marker="s", + label="CFO применяется всегда", +) + +axes[0, 1].semilogy( + large_eb_values, + [ + max( + result["fer_guarded_cfo"], + measurement_floor, + ) + for result in large_cfo_results + ], + marker="^", + label="Защищённая CFO-коррекция", +) + +axes[0, 1].set_xlabel( + "Eb/N0, дБ" +) + +axes[0, 1].set_ylabel( + "FER" +) + +axes[0, 1].set_title( + "Случайный CFO в диапазоне ±1000 Гц" +) + +axes[0, 1].grid( + True, + which="both", +) + +axes[0, 1].legend() + + +# ------------------------------------------------------------ +# 3. Доля включения CFO-коррекции +# ------------------------------------------------------------ + +for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ +): + + selected_results = [ + result + for result in results + if ( + result["max_abs_cfo_hz"] + == max_abs_cfo_hz + ) + ] + + axes[1, 0].plot( + [ + result["eb_n0_db"] + for result in selected_results + ], + [ + result["guarded_apply_rate"] + for result in selected_results + ], + marker="o", + label=( + f"CFO ±{max_abs_cfo_hz:.0f} Гц" + ), + ) + +axes[1, 0].set_xlabel( + "Eb/N0, дБ" +) + +axes[1, 0].set_ylabel( + "Доля включения CFO-коррекции" +) + +axes[1, 0].set_ylim( + -0.03, + 1.03, +) + +axes[1, 0].set_title( + "Решения защищённого корректора" +) + +axes[1, 0].grid( + True +) + +axes[1, 0].legend() + + +# ------------------------------------------------------------ +# 4. Ошибка оценки CFO +# ------------------------------------------------------------ + +for max_abs_cfo_hz in ( + MAX_ABS_CFO_VALUES_HZ +): + + selected_results = [ + result + for result in results + if ( + result["max_abs_cfo_hz"] + == max_abs_cfo_hz + ) + ] + + axes[1, 1].plot( + [ + result["eb_n0_db"] + for result in selected_results + ], + [ + result["frequency_rmse_hz"] + for result in selected_results + ], + marker="o", + label=( + f"CFO ±{max_abs_cfo_hz:.0f} Гц" + ), + ) + +axes[1, 1].set_xlabel( + "Eb/N0, дБ" +) + +axes[1, 1].set_ylabel( + "CFO RMSE, Гц" +) + +axes[1, 1].set_title( + "Точность оценки частотной ошибки" +) + +axes[1, 1].grid( + True +) + +axes[1, 1].legend() + + +figure.tight_layout() + +figure.savefig( + GRAPH_PATH, + dpi=160, +) + +plt.close( + figure +) + + +# ============================================================ +# Текстовый отчёт +# ============================================================ + +report_lines = [ + "Lab023. Guarded CFO correction", + "", + ( + "Trials per point: " + f"{TRIALS_PER_POINT}" + ), + ( + "CFO dead zone: " + f"{CFO_DEAD_ZONE_HZ:.2f} Hz" + ), + ( + "Minimum phase consistency: " + f"{MINIMUM_PHASE_CONSISTENCY:.3f}" + ), + "", +] + +for result in results: + + report_lines.extend( + [ + ( + "Eb/N0: " + f"{result['eb_n0_db']:.1f} dB" + ), + ( + "Maximum absolute CFO: " + f"{result['max_abs_cfo_hz']:.1f} Hz" + ), + ( + "FER phase only: " + f"{result['fer_phase_only']:.6f}" + ), + ( + "FER always CFO: " + f"{result['fer_always_cfo']:.6f}" + ), + ( + "FER guarded CFO: " + f"{result['fer_guarded_cfo']:.6f}" + ), + ( + "Guard apply rate: " + f"{result['guarded_apply_rate']:.6f}" + ), + ( + "CFO RMSE: " + f"{result['frequency_rmse_hz']:.3f} Hz" + ), + "", + ] + ) + +REPORT_PATH.write_text( + "\n".join( + report_lines + ), + encoding="utf-8", +) + + +# ============================================================ +# Автоматические проверки +# ============================================================ + +result_10_db_zero_cfo = next( + result + for result in results + if ( + result["eb_n0_db"] == 10.0 + and result["max_abs_cfo_hz"] == 0.0 + ) +) + +result_10_db_large_cfo = next( + result + for result in results + if ( + result["eb_n0_db"] == 10.0 + and result["max_abs_cfo_hz"] == 1000.0 + ) +) + +assert len(transmitted_frame_bits) == 320 + +# При реальном CFO = 0 защищённый режим +# не должен постоянно включать частотную коррекцию. +assert ( + result_10_db_zero_cfo[ + "guarded_apply_rate" + ] + < 0.25 +) + +# При большом CFO защищённый корректор должен +# быть намного лучше режима только с постоянной фазой. +assert ( + result_10_db_large_cfo[ + "success_guarded_cfo" + ] + > result_10_db_large_cfo[ + "success_phase_only" + ] +) + +# При хорошем SNR защищённый режим должен сохранять +# практически все кадры. +assert ( + result_10_db_large_cfo[ + "success_guarded_cfo" + ] + >= int( + TRIALS_PER_POINT * 0.90 + ) +) + +assert CSV_PATH.exists() +assert GRAPH_PATH.exists() +assert REPORT_PATH.exists() + + +print("\nCSV:") + +print(CSV_PATH) + +print("\nГрафик:") + +print(GRAPH_PATH) + +print("\nОтчёт:") + +print(REPORT_PATH) + +print( + "\nПроверка пройдена: " + "мёртвая зона исключает лишнюю CFO-коррекцию, " + "но сохраняет компенсацию значительного рассогласования." +) \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab024a_pluto_rx_capture.py b/Projects/SDR_Rover/tests/lab024a_pluto_rx_capture.py new file mode 100644 index 0000000..3efcd63 --- /dev/null +++ b/Projects/SDR_Rover/tests/lab024a_pluto_rx_capture.py @@ -0,0 +1,172 @@ +""" +Lab024A. Первый приём реальных IQ-сэмплов с Pluto+. + +Схема: + антенна 40–860 МГц -> RX1 + +Передатчики TX1 и TX2 не используются. +""" + +from pathlib import Path +import json + +import adi +import matplotlib.pyplot as plt +import numpy as np + + +# --------------------------------------------------------------------- +# Параметры приёмника +# --------------------------------------------------------------------- + +PLUTO_URI = "ip:192.168.2.1" + +CENTER_FREQUENCY_HZ = 100_000_000 +SAMPLE_RATE_HZ = 2_400_000 +RX_BANDWIDTH_HZ = 2_000_000 + +RX_BUFFER_SIZE = 262_144 + +OUTPUT_DIRECTORY = Path("data/raw") +IQ_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz.npy" +METADATA_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz.json" +SPECTRUM_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz_spectrum.png" + + +def calculate_spectrum( + samples: np.ndarray, + sample_rate_hz: float, + center_frequency_hz: float, +) -> tuple[np.ndarray, np.ndarray]: + """ + Рассчитывает спектр принятого комплексного IQ-сигнала. + + Возвращает: + frequencies_hz — абсолютные радиочастоты; + power_db — относительная мощность спектра в дБ. + """ + + sample_count = len(samples) + + window = np.hanning(sample_count) + windowed_samples = samples * window + + spectrum = np.fft.fftshift(np.fft.fft(windowed_samples)) + power = np.abs(spectrum) ** 2 + + power_db = 10.0 * np.log10(power + 1e-12) + power_db -= np.max(power_db) + + baseband_frequencies_hz = np.fft.fftshift( + np.fft.fftfreq(sample_count, d=1.0 / sample_rate_hz) + ) + + absolute_frequencies_hz = ( + center_frequency_hz + baseband_frequencies_hz + ) + + return absolute_frequencies_hz, power_db + + +def main() -> None: + OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True) + + print("Подключение к Pluto+...") + sdr = adi.Pluto(uri=PLUTO_URI) + + # Используем только первый приёмный канал RX1. + sdr.rx_enabled_channels = [0] + + sdr.sample_rate = SAMPLE_RATE_HZ + sdr.rx_lo = CENTER_FREQUENCY_HZ + sdr.rx_rf_bandwidth = RX_BANDWIDTH_HZ + + # Автоматическая регулировка усиления. + sdr.gain_control_mode_chan0 = "slow_attack" + + sdr.rx_buffer_size = RX_BUFFER_SIZE + + print() + print("Параметры приёмника:") + print(f" URI: {PLUTO_URI}") + print(f" Центральная частота: {sdr.rx_lo / 1e6:.3f} МГц") + print(f" Частота дискретизации: {sdr.sample_rate / 1e6:.3f} Мвыб/с") + print(f" Полоса RX: {sdr.rx_rf_bandwidth / 1e6:.3f} МГц") + print(f" Режим усиления: {sdr.gain_control_mode_chan0}") + print(f" Размер буфера: {sdr.rx_buffer_size} отсчётов") + + print() + print("Получение IQ-сэмплов...") + + # Первый буфер после перенастройки иногда содержит переходный процесс. + _ = sdr.rx() + + # Второй буфер сохраняем и анализируем. + samples = np.asarray(sdr.rx(), dtype=np.complex64) + + print("IQ-сэмплы получены.") + + np.save(IQ_FILE_PATH, samples) + + mean_value = np.mean(samples) + rms_value = np.sqrt(np.mean(np.abs(samples) ** 2)) + peak_value = np.max(np.abs(samples)) + + metadata = { + "pluto_uri": PLUTO_URI, + "center_frequency_hz": int(sdr.rx_lo), + "sample_rate_hz": int(sdr.sample_rate), + "rx_bandwidth_hz": int(sdr.rx_rf_bandwidth), + "gain_control_mode": sdr.gain_control_mode_chan0, + "sample_count": int(len(samples)), + "sample_dtype": str(samples.dtype), + "mean_i": float(np.real(mean_value)), + "mean_q": float(np.imag(mean_value)), + "rms": float(rms_value), + "peak": float(peak_value), + } + + with METADATA_FILE_PATH.open("w", encoding="utf-8") as metadata_file: + json.dump(metadata, metadata_file, ensure_ascii=False, indent=4) + + frequencies_hz, power_db = calculate_spectrum( + samples=samples, + sample_rate_hz=float(sdr.sample_rate), + center_frequency_hz=float(sdr.rx_lo), + ) + + plt.figure(figsize=(12, 6)) + plt.plot(frequencies_hz / 1e6, power_db) + + plt.title("Lab024A. Спектр сигнала, принятого Pluto+") + plt.xlabel("Частота, МГц") + plt.ylabel("Относительная мощность, дБ") + plt.grid(True) + plt.ylim(-100, 5) + plt.tight_layout() + + plt.savefig(SPECTRUM_FILE_PATH, dpi=150) + plt.show() + + print() + print("Статистика:") + print(f" Количество сэмплов: {len(samples)}") + print(f" Тип данных: {samples.dtype}") + print(f" Среднее I: {np.real(mean_value):.3f}") + print(f" Среднее Q: {np.imag(mean_value):.3f}") + print(f" RMS: {rms_value:.3f}") + print(f" Пиковая амплитуда: {peak_value:.3f}") + + print() + print("Созданы файлы:") + print(f" IQ: {IQ_FILE_PATH}") + print(f" Метаданные:{METADATA_FILE_PATH}") + print(f" Спектр: {SPECTRUM_FILE_PATH}") + + print() + print("Lab024A выполнена успешно.") + + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/Projects/SDR_Rover/tests/lab024b_pluto_welch_spectrum.py b/Projects/SDR_Rover/tests/lab024b_pluto_welch_spectrum.py new file mode 100644 index 0000000..eb7e62b --- /dev/null +++ b/Projects/SDR_Rover/tests/lab024b_pluto_welch_spectrum.py @@ -0,0 +1,573 @@ +""" +Lab024B. Усреднённый спектр Pluto+ методом Уэлча. + +Схема подключения: + антенна 40–860 МГц -> RX1 + +Передатчики TX1 и TX2 не используются. +""" + +from pathlib import Path +import csv + +import adi +import matplotlib.pyplot as plt +import numpy as np +from scipy.signal import find_peaks, welch + + +# --------------------------------------------------------------------- +# Настройки Pluto+ +# --------------------------------------------------------------------- + +PLUTO_URI = "ip:192.168.2.1" + +CENTER_FREQUENCY_HZ = 100_000_000 +SAMPLE_RATE_HZ = 2_400_000 +RX_BANDWIDTH_HZ = 2_000_000 + +RX_BUFFER_SIZE = 65_536 +BUFFER_COUNT = 16 +DISCARD_BUFFER_COUNT = 2 + + +# --------------------------------------------------------------------- +# Настройки спектрального анализа +# --------------------------------------------------------------------- + +WELCH_SEGMENT_LENGTH = 8_192 +WELCH_OVERLAP_LENGTH = 4_096 + +SMOOTHING_BANDWIDTH_HZ = 40_000 + +MINIMUM_PEAK_DISTANCE_HZ = 120_000 +MINIMUM_PEAK_PROMINENCE_DB = 4.0 +MINIMUM_PEAK_HEIGHT_DB = -25.0 + +DC_EXCLUSION_HALF_WIDTH_HZ = 40_000 + +MAXIMUM_CANDIDATE_COUNT = 12 + + +# --------------------------------------------------------------------- +# Выходные файлы +# --------------------------------------------------------------------- + +OUTPUT_DIRECTORY = Path("data/processed/lab024b") + +SPECTRUM_CSV_PATH = OUTPUT_DIRECTORY / "lab024b_welch_spectrum.csv" +CANDIDATES_CSV_PATH = OUTPUT_DIRECTORY / "lab024b_fm_candidates.csv" +GRAPH_PATH = OUTPUT_DIRECTORY / "lab024b_welch_spectrum.png" +REPORT_PATH = OUTPUT_DIRECTORY / "lab024b_report.txt" + + +def configure_receiver() -> adi.Pluto: + """ + Подключается к Pluto+ и настраивает приёмник RX1. + """ + + print("Подключение к Pluto+...") + + sdr = adi.Pluto(uri=PLUTO_URI) + + # Используем первый приёмный канал. + sdr.rx_enabled_channels = [0] + + sdr.sample_rate = SAMPLE_RATE_HZ + sdr.rx_lo = CENTER_FREQUENCY_HZ + sdr.rx_rf_bandwidth = RX_BANDWIDTH_HZ + + # Автоматическая регулировка усиления. + sdr.gain_control_mode_chan0 = "slow_attack" + + sdr.rx_buffer_size = RX_BUFFER_SIZE + + return sdr + + +def receive_samples(sdr: adi.Pluto) -> np.ndarray: + """ + Получает несколько последовательных IQ-буферов. + + Первые буферы отбрасываются, поскольку сразу после настройки + приёмника могут наблюдаться переходные процессы АРУ и фильтров. + """ + + print() + print("Отбрасывание переходных буферов...") + + for _ in range(DISCARD_BUFFER_COUNT): + _ = sdr.rx() + + received_buffers: list[np.ndarray] = [] + + print("Получение рабочих IQ-буферов...") + + for buffer_number in range(1, BUFFER_COUNT + 1): + samples = np.asarray(sdr.rx(), dtype=np.complex64) + received_buffers.append(samples) + + print( + f" Буфер {buffer_number:02d}/{BUFFER_COUNT}: " + f"{len(samples)} отсчётов" + ) + + combined_samples = np.concatenate(received_buffers) + + if len(combined_samples) == 0: + raise RuntimeError("Pluto+ не вернул IQ-сэмплы.") + + if not np.all(np.isfinite(combined_samples)): + raise RuntimeError("В IQ-буфере обнаружены NaN или Inf.") + + return combined_samples + + +def calculate_welch_spectrum( + samples: np.ndarray, + sample_rate_hz: float, + center_frequency_hz: float, +) -> tuple[np.ndarray, np.ndarray]: + """ + Рассчитывает спектральную плотность мощности методом Уэлча. + + Возвращает: + absolute_frequencies_hz — абсолютные радиочастоты; + power_density — мощность в линейном масштабе. + """ + + baseband_frequencies_hz, power_density = welch( + samples, + fs=sample_rate_hz, + window="hann", + nperseg=WELCH_SEGMENT_LENGTH, + noverlap=WELCH_OVERLAP_LENGTH, + detrend=False, + return_onesided=False, + scaling="density", + ) + + baseband_frequencies_hz = np.fft.fftshift( + baseband_frequencies_hz + ) + + power_density = np.fft.fftshift(power_density) + + absolute_frequencies_hz = ( + center_frequency_hz + baseband_frequencies_hz + ) + + return absolute_frequencies_hz, power_density + + +def smooth_power_spectrum( + power_density: np.ndarray, + frequency_step_hz: float, +) -> np.ndarray: + """ + Сглаживает спектр скользящим средним в линейном масштабе. + + Усреднять следует мощность, а не значения в децибелах. + """ + + smoothing_bin_count = int( + round(SMOOTHING_BANDWIDTH_HZ / frequency_step_hz) + ) + + smoothing_bin_count = max(1, smoothing_bin_count) + + # Нечётная длина ядра обеспечивает симметрию относительно центра. + if smoothing_bin_count % 2 == 0: + smoothing_bin_count += 1 + + kernel = ( + np.ones(smoothing_bin_count, dtype=np.float64) + / smoothing_bin_count + ) + + smoothed_power = np.convolve( + power_density, + kernel, + mode="same", + ) + + return smoothed_power + + +def convert_power_to_relative_db( + raw_power: np.ndarray, + smoothed_power: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + """ + Переводит мощность в относительные децибелы. + + 0 дБ соответствует максимальному значению сглаженного спектра. + """ + + minimum_positive_value = np.finfo(np.float64).tiny + + raw_power_db = 10.0 * np.log10( + raw_power + minimum_positive_value + ) + + smoothed_power_db = 10.0 * np.log10( + smoothed_power + minimum_positive_value + ) + + reference_power_db = np.max(smoothed_power_db) + + raw_relative_db = raw_power_db - reference_power_db + smoothed_relative_db = smoothed_power_db - reference_power_db + + return raw_relative_db, smoothed_relative_db + + +def detect_signal_candidates( + frequencies_hz: np.ndarray, + smoothed_power_db: np.ndarray, + center_frequency_hz: float, +) -> list[dict[str, float]]: + """ + Ищет локальные максимумы сглаженного спектра. + + Максимумы сначала рассчитываются по исходному спектру. + После этого отбрасываются точки из DC-зоны и с краёв диапазона. + + Такой порядок не создаёт искусственных провалов -200 дБ + и не искажает значение prominence. + """ + + frequency_step_hz = float( + np.mean(np.diff(frequencies_hz)) + ) + + minimum_distance_bins = max( + 1, + int( + round( + MINIMUM_PEAK_DISTANCE_HZ + / abs(frequency_step_hz) + ) + ), + ) + + edge_bin_count = max( + 1, + int( + round( + SMOOTHING_BANDWIDTH_HZ + / abs(frequency_step_hz) + ) + ), + ) + + # Важно: find_peaks получает настоящий спектр без вставок -200 дБ. + peak_indices, peak_properties = find_peaks( + smoothed_power_db, + height=MINIMUM_PEAK_HEIGHT_DB, + prominence=MINIMUM_PEAK_PROMINENCE_DB, + distance=minimum_distance_bins, + ) + + candidates: list[dict[str, float]] = [] + + for peak_number, peak_index in enumerate(peak_indices): + frequency_hz = float(frequencies_hz[peak_index]) + + # Отбрасываем возможный аппаратный DC-пик. + if ( + abs(frequency_hz - center_frequency_hz) + <= DC_EXCLUSION_HALF_WIDTH_HZ + ): + continue + + # Отбрасываем максимумы возле краёв наблюдаемого диапазона. + if peak_index < edge_bin_count: + continue + + if peak_index >= len(smoothed_power_db) - edge_bin_count: + continue + + candidates.append( + { + "frequency_hz": frequency_hz, + "relative_power_db": float( + smoothed_power_db[peak_index] + ), + "prominence_db": float( + peak_properties["prominences"][peak_number] + ), + } + ) + + # Оставляем наиболее выраженные сигналы. + candidates.sort( + key=lambda item: item["prominence_db"], + reverse=True, + ) + + candidates = candidates[:MAXIMUM_CANDIDATE_COUNT] + + # В итоговой таблице располагаем сигналы по частоте. + candidates.sort( + key=lambda item: item["frequency_hz"] + ) + + return candidates + +def save_candidates_csv( + candidates: list[dict[str, float]], +) -> None: + """ + Сохраняет найденные кандидаты на радиосигналы. + """ + + with CANDIDATES_CSV_PATH.open( + "w", + encoding="utf-8", + newline="", + ) as csv_file: + writer = csv.writer(csv_file) + + writer.writerow( + [ + "frequency_hz", + "frequency_mhz", + "relative_power_db", + "prominence_db", + ] + ) + + for candidate in candidates: + writer.writerow( + [ + f"{candidate['frequency_hz']:.3f}", + f"{candidate['frequency_hz'] / 1e6:.6f}", + f"{candidate['relative_power_db']:.3f}", + f"{candidate['prominence_db']:.3f}", + ] + ) + + +def save_report( + sample_count: int, + candidates: list[dict[str, float]], +) -> None: + """ + Создаёт текстовый отчёт лабораторной. + """ + + report_lines = [ + "Lab024B. Усреднённый спектр Pluto+ методом Уэлча", + "", + f"URI: {PLUTO_URI}", + f"Центральная частота: {CENTER_FREQUENCY_HZ / 1e6:.3f} МГц", + f"Частота дискретизации: {SAMPLE_RATE_HZ / 1e6:.3f} Мвыб/с", + f"Полоса RX: {RX_BANDWIDTH_HZ / 1e6:.3f} МГц", + f"Количество IQ-сэмплов: {sample_count}", + f"Размер сегмента Уэлча: {WELCH_SEGMENT_LENGTH}", + f"Перекрытие сегментов: {WELCH_OVERLAP_LENGTH}", + "", + f"Найдено кандидатов: {len(candidates)}", + "", + ] + + for candidate_number, candidate in enumerate( + candidates, + start=1, + ): + report_lines.append( + f"{candidate_number:02d}. " + f"{candidate['frequency_hz'] / 1e6:.6f} МГц, " + f"уровень {candidate['relative_power_db']:.2f} дБ, " + f"выраженность {candidate['prominence_db']:.2f} дБ" + ) + + REPORT_PATH.write_text( + "\n".join(report_lines), + encoding="utf-8", + ) + + +def create_graph( + frequencies_hz: np.ndarray, + raw_power_db: np.ndarray, + smoothed_power_db: np.ndarray, + candidates: list[dict[str, float]], +) -> None: + """ + Строит исходный и сглаженный спектры. + """ + + plt.figure(figsize=(13, 7)) + + plt.plot( + frequencies_hz / 1e6, + raw_power_db, + linewidth=0.6, + alpha=0.45, + label="Спектр Уэлча", + ) + + plt.plot( + frequencies_hz / 1e6, + smoothed_power_db, + linewidth=1.5, + label="Сглаженный спектр", + ) + + for candidate in candidates: + frequency_mhz = candidate["frequency_hz"] / 1e6 + power_db = candidate["relative_power_db"] + + plt.scatter( + [frequency_mhz], + [power_db], + marker="o", + ) + + plt.annotate( + f"{frequency_mhz:.3f}", + xy=(frequency_mhz, power_db), + xytext=(0, 10), + textcoords="offset points", + ha="center", + fontsize=8, + rotation=45, + ) + + plt.axvspan( + (CENTER_FREQUENCY_HZ - DC_EXCLUSION_HALF_WIDTH_HZ) / 1e6, + (CENTER_FREQUENCY_HZ + DC_EXCLUSION_HALF_WIDTH_HZ) / 1e6, + alpha=0.15, + label="Исключённая DC-зона", + ) + + plt.title( + "Lab024B. Усреднённый спектр Pluto+ и кандидаты на FM-сигналы" + ) + plt.xlabel("Частота, МГц") + plt.ylabel("Относительная мощность, дБ") + plt.grid(True) + plt.ylim(-60, 5) + plt.legend() + plt.tight_layout() + + plt.savefig(GRAPH_PATH, dpi=160) + plt.show() + + +def main() -> None: + OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True) + + sdr = configure_receiver() + + print() + print("Параметры приёмника:") + print(f" URI: {PLUTO_URI}") + print( + f" Центральная частота: " + f"{sdr.rx_lo / 1e6:.3f} МГц" + ) + print( + f" Частота дискретизации: " + f"{sdr.sample_rate / 1e6:.3f} Мвыб/с" + ) + print( + f" Полоса RX: " + f"{sdr.rx_rf_bandwidth / 1e6:.3f} МГц" + ) + print( + f" Режим усиления: " + f"{sdr.gain_control_mode_chan0}" + ) + + samples = receive_samples(sdr) + + print() + print(f"Всего получено: {len(samples)} IQ-сэмплов") + + frequencies_hz, power_density = calculate_welch_spectrum( + samples=samples, + sample_rate_hz=float(sdr.sample_rate), + center_frequency_hz=float(sdr.rx_lo), + ) + + frequency_step_hz = float( + np.mean(np.diff(frequencies_hz)) + ) + + smoothed_power = smooth_power_spectrum( + power_density=power_density, + frequency_step_hz=frequency_step_hz, + ) + + raw_power_db, smoothed_power_db = ( + convert_power_to_relative_db( + raw_power=power_density, + smoothed_power=smoothed_power, + ) + ) + + candidates = detect_signal_candidates( + frequencies_hz=frequencies_hz, + smoothed_power_db=smoothed_power_db, + center_frequency_hz=float(sdr.rx_lo), + ) + + save_spectrum_csv( + frequencies_hz=frequencies_hz, + raw_power_db=raw_power_db, + smoothed_power_db=smoothed_power_db, + ) + + save_candidates_csv(candidates) + save_report(len(samples), candidates) + + print() + print("Найденные кандидаты на радиосигналы:") + print() + + if candidates: + print( + " № Частота, МГц Уровень, дБ " + "Выраженность, дБ" + ) + print( + " -- ------------ ----------- " + "-----------------" + ) + + for candidate_number, candidate in enumerate( + candidates, + start=1, + ): + print( + f" {candidate_number:2d} " + f"{candidate['frequency_hz'] / 1e6:12.6f} " + f"{candidate['relative_power_db']:11.2f} " + f"{candidate['prominence_db']:17.2f}" + ) + else: + print(" Кандидаты не найдены.") + + create_graph( + frequencies_hz=frequencies_hz, + raw_power_db=raw_power_db, + smoothed_power_db=smoothed_power_db, + candidates=candidates, + ) + + print() + print("Созданы файлы:") + print(f" Спектр: {SPECTRUM_CSV_PATH}") + print(f" Кандидаты: {CANDIDATES_CSV_PATH}") + print(f" График: {GRAPH_PATH}") + print(f" Отчёт: {REPORT_PATH}") + + print() + print("Lab024B выполнена успешно.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Projects/SDR_Rover/tools/.gitkeep b/Projects/SDR_Rover/tools/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Projects/SDR_Rover/tools/__init__.py b/Projects/SDR_Rover/tools/__init__.py new file mode 100644 index 0000000..633f866 --- /dev/null +++ b/Projects/SDR_Rover/tools/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- + diff --git a/Projects/SDR_Rover/tools/virtual_channel.py b/Projects/SDR_Rover/tools/virtual_channel.py new file mode 100644 index 0000000..e4179f3 --- /dev/null +++ b/Projects/SDR_Rover/tools/virtual_channel.py @@ -0,0 +1,72 @@ +""" +Виртуальный канал связи SDR Rover Link. + +Имитирует: +1. Полную потерю пакета. +2. Случайное изменение отдельных битов. +""" + +import random + + +def transmit( + packet: bytes, + drop_probability: float, + bit_error_probability: float, + random_generator: random.Random, +): + """ + Передать пакет через виртуальный канал. + + Возвращает: + received_packet: + Пакет после прохождения канала. + None означает полную потерю пакета. + + flipped_bits: + Количество изменённых битов. + """ + + if not isinstance(packet, (bytes, bytearray)): + raise TypeError( + "packet должен иметь тип bytes или bytearray" + ) + + if not 0.0 <= drop_probability <= 1.0: + raise ValueError( + "drop_probability должен находиться " + "в диапазоне 0.0...1.0" + ) + + if not 0.0 <= bit_error_probability <= 1.0: + raise ValueError( + "bit_error_probability должен находиться " + "в диапазоне 0.0...1.0" + ) + + # Решаем, потерян ли пакет полностью. + if random_generator.random() < drop_probability: + return None, 0 + + corrupted_packet = bytearray(packet) + flipped_bits = 0 + + # Перебираем каждый байт. + for byte_index in range(len(corrupted_packet)): + + # В каждом байте восемь битов. + for bit_index in range(8): + + if ( + random_generator.random() + < bit_error_probability + ): + bit_mask = 1 << bit_index + + # XOR меняет выбранный бит: + # 0 → 1 или 1 → 0. + corrupted_packet[byte_index] ^= bit_mask + + flipped_bits += 1 + + return bytes(corrupted_packet), flipped_bits \ No newline at end of file