Вы находитесь на странице: 1из 57

Федеральное государственное образовательное бюджетное учреждение

высшего образования

«Финансовый университет при Правительстве Российской


Федерации» (Финансовый университет)

Факультет прикладной̆ математики и информационных технологий

Департамент анализа данных, принятия решений и финансовых


технологий

Выпускная квалификационная работа на тему:

«Моделирование крупномасштабных сетевых структур в транспортно-


логистических системах»

Направление подготовки 01.03.02 «Прикладная математика и


информатика», Профиль «Анализ данных и принятие решений в экономике и
финансах»

Выполнил студент группы ПМ18-1

Намлеев Валентин Вадимович

Научный руководитель

к. э. н., доцент, Кочкаров Р. А.

ВКР соответствует предъявляемым требованиям

Руководитель Департамента

д.э.н., профессор _____________ В.И. Соловьев «__» ___________2022


г.

Москва 2022
ОГЛАВЛЕНИЕ

ОГЛАВЛЕНИЕ ............................................................................................ 2

ВВЕДЕНИЕ .................................................................................................. 3

1. МОДЕЛИРОВАНИЕ СЕТЕВЫХ СТРУКТУР .................................... 5

1.1. Моделирование в логистике ........................................................................................... 5

1.2. Обзор рынка транспортно-логистических решений ................................................. 9

1.3. Математическая модель транспортно-логистической сети................................... 14

2. ЖЕЛЕЗНОДОРОЖНЫЕ ЛОГИСТИЧЕСКИЕ СИСТЕМЫ ........... 20

2.1. Первичный анализ данных........................................................................................... 20

2.2. Предобработка и анализ данных ................................................................................. 25

2.3. Постановка многокритериальной задачи оптимизации......................................... 29

2.4. Разработка алгоритма решения поставленной задачи. .......................................... 29

3. Поиск решения....................................................................................... 32

3.1. Решение поставленной задачи ..................................................................................... 32

3.2. Сравнительный анализ эффективности разработанного алгоритма .................. 36

Заключение ................................................................................................. 38

СПИСОК ИСТОЧНИКОВ ....................................................................... 39

ПРИЛОЖЕНИЯ ........................................................................................ 40

Приложение 1. Скриншоты разработанного ПО. ........................................................... 40

Приложение 2. Программный код разработанного ПО ................................................. 41

2
ВВЕДЕНИЕ

Логистика чаще всего понимается как наука об оптимальном


управлении потоками ресурсов необходимых для производства услуг и
товаров.

Любой товар связан с огромным количеством логистических B2B


(business-business) и B2C (business-customer) предприятий. Логистика
управляет, множеством горнодобывающих комбинатов, сельхозугодий,
цепочек производств, поставок до прилавков магазинов, точек выдачи
интернет-заказов и курьеров доставки. Финальная стоимость товара или
услуги складывается из всех издержек, появляющихся на протяжении их
жизненного цикла. Несогласованность взаимодействия любых фрагментов
логистической цепочки приводит к задержкам производства, увеличению
затрат, срыву контрактов, репутационным потерям. Как следствие, логистика
является наиважнейшим инструментом управления бизнесом.

У логистики до сих пор не существует одного общепринятого


определения. Большинство из множества ее определений описывает
определенную прикладную логистическую область, в каждой из которых
сформировалась своя дисциплина. Перечислим наиболее известные из них:

• Логистика закупок
• Сбытовая логистика
• Транспортная логистика
• Таможенная логистика
• Логистика запасов
• Складская логистика
• Информационная логистика
• Комплексная логистика

3
Не смотря на широкое функциональное разнообразие логистики, в
каждой из них можно выделить четыре фундаментальные задачи –
планирование, оптимизация, управление и контроль. Естественно, каждая из
них также имеет свои специфические задачи.

В этой работе более подробно рассмотрена проблематика транспортной


логистики, как самой широкой и развитой дисциплины. Большая часть
логистических компаний занимается именно транспортной логистикой.

В широком понимании, транспортно-логистическая система -


совокупность объектов и субъектов транспортной и логистической
инфраструктуры вместе с материальными, финансовыми и информационными
потоками между ними, выполняющая функции транспортировки, хранения,
распределения товаров, а также информационного и правового
сопровождения товарных потоков. В этих системах выделяют три основные
цели:

1. финансовые, которые выражаются в форме прибыли при


рентабельности и ликвидности;
2. производственно-технические, которые выражаются общей
производительностью и производительностью отдельных
подразделений, минимизацией отдельных периодов времени в
процессе производства;
3. техническая эффективность, т. е. технические параметры и
ресурсоемкость производства, и др.

В условиях развития рыночных отношений для транспортных


предприятий основной целью деятельности является качественное
обслуживание грузовладельцев. В условиях сокращения объема перевозок и
роста конкуренции со стороны других видов транспорта (прежде всего
автомобильного) любому виду транспорта для получения прибыли
необходимо применять новые технологии.

4
1. МОДЕЛИРОВАНИЕ СЕТЕВЫХ СТРУКТУР

1.1. Моделирование в логистике

Рассмотрим моделирование в контексте транспортной логистики.


Моделирование основывается на подобии систем или процессов, которое
может быть полным или частичным. Основная цель моделирования - прогноз
поведения процесса или системы.

Существенной характеристикой любой модели является степень


полноты подобия модели моделируемому объекту. По этому признаку все
модели можно подразделить на изоморфные и гомоморфные.

Изоморфные модели — это модели, включающие все характеристики


объекта-оригинала, способные, по существу, заменить его. Если можно
создать и наблюдать изоморфную модель, то наши знания о реальном объекте
будут точными. В этом случае мы сможем точно предсказать поведение
объекта.

Гомоморфные модели. В их основе лежит неполное, частичное подобие


модели изучаемому объекту. При этом некоторые стороны функционирования
реального объекта не моделируются совсем. В результате упрощаются
построение модели и интерпретация результатов исследования. При
моделировании логистических систем абсолютное подобие не имеет места.
Поэтому в дальнейшем мы будем рассматривать лишь гомоморфные модели,
не забывая, однако, что степень подобия у них может быть различной.

Существует два основных подхода к моделированию логистических


систем. Аналитическое моделирование — это математический прием
исследования логистических систем, позволяющий получать точные решения.
Аналитическое моделирование осуществляется в следующем порядке:

5
Формулируются математические законы, связывающие объекты
системы. Эти законы записываются в виде некоторых функциональных
соотношений (алгебраических, дифференциальных и т.п.).

• Решение уравнений, получение теоретических результатов.


• Сопоставление полученных теоретических результатов с
практикой (проверка на адекватность).

Наиболее полное исследование процесса функционирования системы


можно провести, если известны явные зависимости, связывающие искомые
характеристики с начальными условиями, параметрами и переменными
системами. Однако такие зависимости удается получить только для
сравнительно простых систем. При усложнении систем исследование их
аналитическими методами наталкивается на определенные трудности, что
является существенным недостатком метода. В этом случае, чтобы
использовать аналитический метод, необходимо существенно упростить
первоначальную модель, чтобы иметь возможность изучить хотя бы общие
свойства системы. К достоинствам аналитического моделирования относят
большую силу обобщения и многократность использования.

Другим видом математического моделирования является имитационное


моделирование. Как уже отмечалось, логистические системы функционируют
в условиях неопределенности окружающей среды. При управлении
материальными потоками должны учитываться факторы, многие из которых
носят случайностный характер. В этих условиях создание аналитической
модели, устанавливающей четкие количественные соотношения между
различными составляющими логистических процессов, может оказаться либо
невозможным, либо слишком дорогим.

При имитационном моделировании закономерности, определяющие


характер количественных отношений внутри логистических процессов,

6
остаются непознанными. В этом плане логистический процесс остается для
экспериментатора «черным ящиком».

Процесс работы с имитационной моделью, заключается в подборе


исходных параметров (условий) модели, а определение таких условий, при
которых результат удовлетворяет требованиям, является целью работы.
Имитационное моделирование можно разделить на две задачи:

1. Конструирование модели реальной системы.


2. Постановка экспериментов на сконструированной модели.

При этом могут преследоваться следующие цели - понять поведение


логистической системы и(или) выбрать стратегию, обеспечивающую
наиболее эффективное функционирование логистической системы.

Имитационное моделирование осуществляется с помощью


компьютеров. Условия, при которых рекомендуется применять имитационное
моделирование, приведены в работе Р. Шеннона «Имитационное
моделирование систем: искусство и наука». Перечислим основные из них:

• Не существует законченной математической постановки данной


задачи, либо еще не разработаны аналитические методы решения
сформулированной математической модели.
• Аналитические модели имеются, но процедуры столь сложны и
трудоемки, что имитационное моделирование дает более простой
способ решения задачи.
• Аналитические решения существуют, но их реализация
невозможна вследствие недостаточной математической
подготовки имеющегося персонала.

Таким образом, основным достоинством имитационного моделирования


является то, что этим методом можно решать более сложные задачи.

7
Имитационные модели позволяют достаточно просто учитывать случайные
воздействия и другие факторы, которые создают трудности при
аналитическом исследовании.

При имитационном моделировании воспроизводится процесс


функционирования системы во времени. Причем имитируются элементарные
явления, составляющие процесс с сохранением их логической структуры и
последовательности протекания во времени. Модели не решают, а
осуществляют многократную симуляцию модели с различными параметрами.

Имитационное моделирование имеет ряд существенных недостатков,


которые также необходимо учитывать:

• Исследования с помощью этого метода обходятся дорого - для


построения модели и экспериментирования на ней необходим
высококвалифицированный специалист-программист, а также
большое количество вычислительных ресурсов, поскольку метод
основывается на статистических испытаниях и требует
многочисленных прогонов программ.
• Модели разрабатываются для конкретных условий. Велика
вероятность ложной имитации. Процессы в логистических
системах носят вероятностный характер и поддаются
моделированию только при введении определенного рода
допущений. Например, разрабатывая имитационную модель
товароснабжения района и принимая среднюю скорость движения
автомобиля на маршруте, равную 25 км/ч, мы исходим из
допущения, что дорожные условия хорошие. В действительности
погода может испортиться и, в результате наступившего гололеда,
скорость на маршруте упадет до 15 км/ч. Реальный процесс пойдет
иначе.

8
Описание достоинств и недостатков имитационного моделирования
можно завершить словами Р. Шеннона: «Разработка и применение
имитационных моделей в большей степени искусство, чем наука.
Следовательно, успех или неудача в большей степени зависит не от метода, а
от того, как он применяется».

Не смотря на различия между описанными выше подходами к


моделированию, основная цель их применения - выявление недостатков,
поиск возможных решений, их проверка на работоспособность и как
следствие, оптимизация моделируемой системы путем применения
найденного решения на практике.

1.2. Обзор рынка транспортно-логистических решений

Транспортная логистика масштабируется от уровня внутрицеховой до


уровня предприятия, города, региона, страны и международного, и решает
следующие задачи – организация планирования, оптимальной
маршрутизации, страхования, управления и контроля перемещения товаров с
использованием автомобильного, железнодорожного, трубопроводного,
морского, речного и авиационного транспорта, перевалка грузов из одних
транспортных средств в другие.

Транспортные компании составляют значительную часть логистических


компаний и выполняют перевозки различного назначения:

• Автоперевозки
• Морские перевозки
• Железнодорожные перевозки грузов
• Мультимодальные перевозки
• Контейнерные перевозки
• Международные перевозки грузов
• Перевозка опасных грузов

9
• Рефрижераторные перевозки
• Перевозка негабаритных грузов
• Перевозка сборных грузов и др.

В 2021 году остро встала проблема дефицита линейного персонала на


складах, транспорта, складских мощностей и технологий. Следствием этого
стало повышение цен на основные логистические ресурсы. Не смотря на тренд
цифровизации и постепенного образования глобального интернета вещей,
многие компании все еще не используют электронную торговую площадку
(ЭТП), а некоторые — так и не научились с ней работать.

Примером компании, не испытавшей трудностей в 2021 году, например


с дефицитом персонала, стал «Decathlon S.A.». Проблем удалось избежать
вследствие хорошей внутренней производственной культуры, лояльности
компании, эргономики труда и роботизации. Не менее болезненной проблемой
в логистике стало повышение цен на обработку интернет-заказов у ритейлеров
и дистрибьюторов при работе с маркетплейсами, и как следствие увеличение
времени исполнения заказа. Эту проблему наглядно демонстрирует ситуация
с товарами из Китая, сложившаяся в 2021 году: срок доставки товаров
увеличился в три раза. Это повлекло за собой снижение оборачиваемости
запасов и оборотного капитала, а также уменьшение объемов продаж из-за
несоответствия фактических запасов и спроса. На рынке сложилась ситуация,
когда склады заполнены товаром, но спрос на них падает, в результате чего
продукция не продается, и возникает нехватка складских помещений.

Наиболее важным фактором, влияющим на время поставки, является


качество планирования. Необходимо повышать открытость и прозрачность в
цепях поставок, поскольку это является залогом конкурентоспособности
компании, а также делиться прогнозами со своими поставщиками, чтобы
выстроить с ними доверительные отношения.

10
Для поиска решения описанных выше проблем необходим инструмент
позволяющий тщательно исследовать транспортно-логистическую систему.
Под это определение отлично подходят методы моделирования, описанные в
предыдущем параграфе. Самым простым решением может показаться
использование одного из готовых программных комплексов имитационного
моделирования. Рассмотрим более подробно наиболее популярные
программы в области имитационного моделирования в логистике.

Одной из универсальных программ для имитационного моделирования


является AnyLogic. Программа представлена XJ Technologies, которая
является одной из немногих российских компаний, которые разрабатывают
программы подобного типа. При помощи данного программного обеспечения
можно составлять долгосрочные и краткосрочные расписания,
минимизировать затраты на любой вид перевозок и содержание автопарка, а
также оптимизировать запасы на базе имитации работы участников цепочки
поставок. Ниже на рис. 1 изображен скриншот с моделированием процесса
транспортировки груза.

Рисунок 1. Моделирование процесса транспортировке в программе


«AnyLogic»

11
Другой программой, которая разработана российскими программистами
для имитационного моделирования в логистике, является Дорожный
менеджер. Она узко специализирована для моделирования транспортных
потоков улично-дорожной сети города, региона и страны в целом. Она
предоставляет функционал, позволяющий увидеть обстановку на дороге,
учитывая факторы, влияющие на скорость и плотность транспортного потока.
На рис. 2 изображен процесс моделирования транспортных потоков в городе,
а именно определения загруженности дорог при задании минимальной и
максимальной скорости движения автотранспорта за определенный
промежуток времени.

Рисунок 2. Моделирование транспортных потоков в программе


«Дорожный менеджер».

Supply Chain Guru – программа для имитационного моделирования,


разрабатываемая LLamaSoft, которая позволяет визуализировать,
оптимизировать и анализировать цепочку поставок. Программа позволяет
уменьшить риск, анализируя изменения, вносимые в цепочку поставок, а
также прогнозировать процессы доставки. На рис. 3 изображена симуляция
транспортного маршрута, в программе Supply Chain Guru.

12
Рисунок 3. Моделирование цепи поставок в программе «Supply Chain
Guru»

У всех вышеописанных программных комплексов, можно выделить


следующие достоинства:

• отсутствуют ограничения на ввод пользовательских данных.


• отсутствуют ограничения на воспроизведение в модели
алгоритмов управления.
• полная свобода действий в области детализирования процессов
имитационной модели.

Как и у любого другого продукта, в них есть и недостатки:

• Значительные расходы организации на сбор первичных данных


для создания нужной модели, воспроизведение имитационной
модели на компьютере (сильно зависит от вычислительных
мощностей), приобретение ПО, на проверку на соответствие
введённых данных модели с существующей системой, на поиск и
заработную плату профессионалов;
• Модель может раскрыться для исследования в полной мере только
для одной системы (исходной).

13
• Для того чтобы построить модель нужен глубокий и детальный
анализ, так как процессы в логистической системе имеют
вероятностный характер и поддаются моделированию только при
определенных условиях.
• Закономерности определяющие взаимоотношения элементов
моделируемой логистической системы остаются неизвестными,
даже при успешном подборе исходных параметров.

Работа с комплексами имитационного моделирования, требует от


исследователя значительного опыта работы с ними. Заметим также, что в силу
значительных различий между описанными выше программными
комплексами, опыт работы с одним из них, не дает большого преимущества в
работе с другими. Описанные выше качества имитационного моделирования,
привели к ситуации дефицита высококвалифицированных специалистов в
этой области.

Как было отмечено ранее, существует и другой подход к моделированию


логистических систем – аналитическое моделирование с помощью
математических моделей.

1.3. Математическая модель транспортно-логистической сети

С появлением интернета и увеличением масштаба социальных групп и


уже существующих экономических систем, появилась и необходимость их
исследования и описания. Все они являются совокупностью элементов и
взаимодействий между ними. Теория графов – математическая теория,
исследующая структуры этих совокупностей и законы взаимодействия между
их элементами. Базовой единицей представления элементов и связей между
ними, является граф.

Граф – совокупность вершин (элементов) и ребер (связей), связывающих


их. Математическую модель графа можно представить в виде двух множеств:

14
𝐺 = {𝑉, 𝐸}

где 𝑉 – множество вершин, в котором 𝑣𝑖 – i-ая вершина, 𝐸 – множество


ребер, где 𝑒𝑖𝑗 – ребро, связывающее вершины 𝑣𝑖 , 𝑣𝑗 . При использовании графа
для моделирования транспортно-логистической системы, соотнесем его
вершины с пунктами доставки товаров, а ребра – с возможностью
перемещения между ними.

Такое определение графа, уже позволяет строить системы с весьма


сложной структурой, но в задачах оптимизации необходимо учитывать не
только структуру, но и некоторые материальные характеристики сущностей,
представляемых ребрами и вершинами. Для добавления этой информации в
модель графа, принято определять веса ребер и вершин, которые отображают
их параметры. Если количество весов больше, чем один, такой набор весов
принято называть вектором весов.

Рассмотрим некоторые основные характеристики графов:

• Ориентированность графа показывает наличие у его ребер, такой


характеристики как направление. Если граф ориентированный, то
по каждому его ребру можно передвигаться лишь в указанном
направлении. Две вершины могут быть связаны двумя ребрами,
которые будут иметь разные направления и веса. Это качество
может быть полезно при моделировании автомобильных
транспортных путей, например если движение в одну сторону
затруднено более, чем в другую.
• Связность определяет возможность добраться из любой вершины
графа в любую другую, иначе говоря, для двух любых вершин

∀𝑣𝑖 , 𝑣𝑗 ∈ 𝑉, 𝑣𝑖 ≠ 𝑣𝑗 ∃ 𝑔 ⊂ 𝐺 ∶ 𝑔𝑜 = 𝑣𝑖 , 𝑔𝑛 = 𝑣𝑗

15
• Полнота графа определяет наличие ребра, соединяющего любые
две вершины, то есть
∀𝑣𝑖 , 𝑣𝑗 ∈ 𝑉, 𝑣𝑖 ≠ 𝑣𝑗 ∃ 𝑒𝑖𝑗 ∈ 𝐸
• Степень вершины графа – количество вершин, с которым
выбранная вершина связана ребрами, другими словами –
количество инцидентных вершин. Вершины со степенью равной
нулю, называются изолированными, единице – висячими.

Оптимизационные задачи, как правило, определяются двумя


составляющими – целевой функцией и системой ограничений. В случае
оптимизационных задач на графах, можно конкретизировать определение
оптимизационной задачи – оптимизационная задача на графе 𝐺, это задача
выбора подграфа 𝑔: 𝐸𝑔 ⊂ 𝐸𝐺 , 𝑉𝑔 ⊂ 𝑉𝐺 , таким образом, что целевая функция 𝑓 →
min(max) принимает оптимальное значение на подграфе 𝑔, и подграф 𝑔
удовлетворяет системе ограничений. Система ограничений может принимать
различные формы в зависимости от конкретной задачи.

Поскольку целью этой работы является моделирование и оптимизация


крупномасштабной сетевой структуры, логично выделить интересные нам
классы оптимизационных задач:

• Поиск минимального остовного дерева. Дерево – граф, не


содержащий циклов. Остовное дерево – подграф данного графа,
включающий все его вершины и не нарушающий его связности.
Задача поиска минимального остовного дерева заключается в
поиске остовного дерева сумма весов ребер которого минимальна.
Задача часто решается при построении дорог между населенными
пунктами, для минимизации строительных издержек.
Для решения чаще всего применяют алгоритмы Прима, Краскала
и Борувки.

16
• Поиск максимального потока, применяющаяся на транспортных
сетях. Транспортная сеть – ориентированный граф, в котором для
каждого ребра 𝑒𝑢𝑣 определена пропускная способность 𝑐(𝑢, 𝑣) ≥
0, а также определены точки источника 𝑠 и стока 𝑡, при этом
любая вершина лежит на пути из 𝑠 в 𝑡. Поток сети - функция 𝑓 вида
𝑉 × 𝑉 → ℝ имеющая свойства:
a. ∀𝑢, 𝑣 ∈ 𝑉, 𝑓(𝑢, 𝑣) ≤ 𝑐(𝑢, 𝑣)
b. 𝑓(𝑢, 𝑣) = −𝑓(𝑣, 𝑢)
𝑉
c. ∀𝑣 ∈ 𝑉, 𝑣 ≠ 𝑠, 𝑣 ≠ 𝑡 ∑𝑤∈𝑉 f(u, w) = 0

Задача нахождения максимального потока заключается в поиске


такого потока, что сумма потоков из истока максимальна.

• Поиск кратчайшего пути – одна из самых широко известных задач


оптимизации на графах (при поиске пути, навигатор решает
именно эту задачу). Существует большое количество способов ее
решения – алгоритмы Дейкстры и его модификации, алгоритм
Беллмана-Форда и др.

В дальнейшей работе будет рассмотрена именно задача поиска


кратчайшего пути, как наиболее распространённая и насущная задача.
Большое количество исследований и научных работ на эту тему, дает
возможность подробно изучить специфику задачи и возможные способы
разработки алгоритма для ее решения. Рассмотрим алгоритм Дейкстры поиска
кратчайшего пути на графе.

Алгоритм работоспособен лишь на графах без ребер с отрицательным


весом, тем не менее находит широкое применение в практике, например в
протоколах маршрутизации сетевых потоков OSPF и IS-IS. Формально
определим алгоритм – даны взвешенный неориентированный граф 𝐺, и его

17
вершина 𝑎. Требуется найти кратчайшие пути до всех остальных вершин этого
графа.

Решение находится следующим образом:

1. Каждой вершине присваивается метка. Если вершина 𝑎, то метка


равна 0, иначе значению метки присваивается ∞.
2. Поочередно просматриваются вершины инцидентные вершине 𝑎.
Если сумма метки вершины 𝑎 и веса инцидентного ребра меньше
метки соседней вершины, заменяем ее на сумму; иначе, оставляем
ее неизменной.
3. Проводим шаг 2 для каждой из инцидентных с 𝑎 вершин.
Повторяем процедуру пока каждой вершине не будет присвоена
метка не равная ∞.

После присвоения меток вершинам, при поиске пути из 𝑎 в другую


вершину, выбираем вершину с минимальной меткой.

1.4. Результаты теоретического исследования.

В результате проведенного теоретического исследования, показана


актуальность логистических задач в современном мире. Также были
проанализированы подходы к моделированию транспортно-логистических
систем. В контексте исследования алгоритмов оптимизации структуры и
решения оптимизационных задач, следует отдать предпочтение методу
аналитического моделирования. Такое решение обусловлено большим
контролем над исследуемой системой и прозрачностью происходящих в ней
процессов, чем при использовании метода имитационного моделирования.

Основой для математической модели транспортно-логистической сети


была выбрана теория графов, предоставляющий широкий набор инструментов

18
для работы с сетевыми структурами. Определен способ построения модели
транспортно-логистической системы на основе графа.

Выбранной областью оптимизационных задач стали задачи поиска


кратчайшего пути. Рассмотрен простейший алгоритм их решения – алгоритм
Дейкстры.

19
2. ЖЕЛЕЗНОДОРОЖНЫЕ ЛОГИСТИЧЕСКИЕ СИСТЕМЫ

Все вычисления, описываемые в этой главе, были произведены с


использованием языка программирования Python.

2.1. Первичный анализ данных

Железнодорожный транспорт является одним из наиболее


перспективных. Широкая сеть железнодорожных путей по всему миру,
предоставляет возможность быстрой доставки крупногабаритных грузов, что
было бы трудоемко сделать, используя другие виды транспорта. После
распространения автомобильного транспорта и увеличения его популярности,
железнодорожный транспорт потерял популярность, но актуальность и
преимущества его использования остаются неизменными.

Для построения модели был выбран набор данных, описывающих


железнодорожные системы всего мира, состоящий примерно из 117 тыс.
записей. Формат, в котором хранится информация о железнодорожных путях
весьма специфичен: каждая запись состоит из семи полей, большинство из
которых бесполезны для целей, поставленных в этой работе. Наиболее
интересными полями являются поля shape и iso3. Первое – список
географических координат одного железнодорожного полотна в формате
строки, второе – код страны в международном формате ISO.

Рисунок 4. Структура выбранного набора данных.

20
В выбранный набор данных входит информация о железнодорожных
путях в 165 странах, включая США, Россию и все страны Европы, то есть
более чем 83% мировых железнодорожных систем.

Так как железнодорожные сети в основном располагаются на суше, их


можно разделить на группы по территориальному признаку, а именно по
принадлежности к материкам. Соответственно, и рассматривать эти группы
вместе не имеет смысла, поскольку они не имеют связей между собой. В
некоторых случаях, например при рассмотрении железнодорожной системы
европейской части Евразии, имеет смысл включить в нее и железнодорожную
систему Великобритании, из-за ее близости к материку. Связи (ребра графа) в
таком случае могут формироваться искусственно. Такое искусственное
изменение модели легко оправдывается тем, что у жителей материка есть
множество способов перебраться на остров, например с помощью водного
транспорта или подземного автомобильного туннеля. Используем описанный
подход, и построим модель железнодорожных путей Европы и России.

Выбранный набор данных представляет собой информацию о сетевой


структуре, поэтому преобразуем его в граф с помощью библиотеки networkx,
предоставляющую удобный интерфейс взаимодействия с графами, а также
множество готовых реализаций типичных алгоритмов на графах.

Для построения графа, необходимо преобразовать строки из поля shape,


в пары чисел долгота, широта. С помощью функционала Python работы со
строками, избавимся от лишних символов оставив лишь строковые
представления чисел с плавающей точкой, а затем применим гибкую систему
типов Python для замены строковых представлений, на представляемые числа
(рис. 5).

21
Рисунок 5. Функция, преобразующая строку из поля shape в
последовательность точек.

Заметим, что проводя описанные выше преобразования, важно


сохранить порядок чисел, поскольку в поле shape каждого столбца,
описывается уникальное железнодорожное полотно, принадлежащее
конкретной стране. Точки в ней следуют в порядке продвижения по полотну,
что должно учитываться при формировании ребер между вершинами.

Результатом работы написанной функции (рис. 6) является


представляющий железнодорожную систему граф, изображенный на рис. 7.

22
Рисунок 6. Функция преобразования данных в граф.

Рисунок 7. Страны графа ж/д путей.

23
Рисунок 8. Компоненты графа ж/д путей.

Как видно из рис. 8, восточная часть железнодорожных путей


Российской Федерации не связана с остальной ее частью. Такое странное
состояние графа, скорее всего объясняется ошибкой в выбранном наборе
данных. Тем не менее, это не является проблемой, так как восточная часть
путей, не будет рассматриваться при решении задачи оптимизации. Западную
и восточную части соединяет всего один путь, который будет являться
«бутылочным горлышком» для задач поиска кратчайшего пути, поэтому
рассматривать эти две части в совокупности не имеет смысла, а восточная
часть в отдельности не обладает сложной структурой, поэтому не
представляет самостоятельного интереса.

Можно также заметить, что в железнодорожных системах большинства


стран, присутствуют множества изолированных вершин. В этой работе такие
изолированные множества учитываться не будут, по причине их малой
значимости. В таблице 1 отображено относительное количество вершин в
наибольшей компоненте графов, представляющих железнодорожные системы

24
стран. Как видно, большая часть железнодорожных систем практически не
имеет несвязных компонентов. Это может говорить о высоком уровне
интереса к железнодорожному транспорту со стороны государства и высоком
уровне контроля над ним, что дало железнодорожному транспорту
возможность расшириться до масштабов страны.

Наименование Доля вершин, Количество Количество Количество


страны находящихся ребер вершин компонентов
в наибольшем
компоненте %
РФ 66% 71660 71250 255
Германия 99% 34253 33424 23
Франция 99% 24094 23758 13
Украина 97% 14862 14670 17
Польша 99% 14348 13958 7
Таблица 1. Соотношение мощностей изолированных множеств к
наибольшему компоненту.

2.2. Предобработка и анализ данных

Результативность и правдивость моделирования напрямую зависит от


качества построенной модели – чем больше различных параметров учитывает
модель, тем более вероятными можно считать результаты моделирования.

Всю систему железнодорожного транспорта можно разделить на


кластеры – множество связанных между собой станций, имеющих схожие
транспортные условия, которые в свою очередь формируются на основании
множества технических и экономических параметров. Выделим несколько из
них:

• Расстояние между станциями.


• Качество железнодорожных составов.

25
• Транспортные издержки.
• Максимальная скорость поезда.

Расстояние между станциями определяет общую длину пути, которая


является самым банальным оптимизируемым параметром в задачах поиска
кратчайшего пути. Специфика железнодорожных транспортных систем, также
накладывает свои ограничения на эти задачи, например железнодорожный
состав либо не может совершать маневры на острых поворотах, либо может
делать это с большой потерей времени. Таким образом при поиске
кратчайшего пути, накладываются существенные ограничения на выбор
следующей точки маршрута.

Качество железнодорожного состава определяет его вместимость, то


есть максимальное количество перевозимого ресурса.

Качество железнодорожного состава и транспортные издержки зависят


от многих других факторов, в том числе и от случайного возмущения, и
непрерывно меняются во времени. Поскольку найти реальные данные
описывающие эти параметры не представляется возможным, чтобы учесть их
влияние, сгенерируем эти параметры для каждого ребра графа
самостоятельно, руководствуясь собственными размышлениями.

Зачастую прослеживается следующая тенденция - качество товаров и


услуг зависит от расстояния до ближайших крупных городов. Например, чем
ближе пользователь сети интернет находится к Москве, тем лучше качество и
скорость связи. Такую же логику можно применить и железнодорожной
системе – чем дальше от ближайшего крупного города находятся
железнодорожные пути, тем хуже качество передвигающихся по нему
составов и меньше транспортные издержки. Последнее более спорно, но в
целом за неимением эмпирических данных, такое приближение будет
рациональным.

26
Не стоит применять такую логику напрямую, все же качество составов
и издержки в действительности не всегда распределяются по такому закону.
Для более корректного расчета параметров в общедоступных источниках была
найдена информация о максимальных скоростях поездов в каждой из стран,
входящих в исследуемую железнодорожную систему. Скорость передвижения
по каждому из ребер и транспортные издержки, будут рассчитаны на основе
найденных данных с учетом описанной выше тенденции и случайного
возмущения. Случайное возмущение сгенерировано с помощью модуля stats
библиотеки scipy.

Рисунок 9. Функция рассчитывающая центральность ребра.

При расчете центральности, для каждой из стран были найдены


соседствующие страны. Соседствующими считались те, с которыми первая
страна имеет общие границы. Коэффициент центральности рассчитывался как
среднее арифметическое расстояний до каждой из столиц соседствующих
стран и страной, в которой находится ребро графа (рис. 9). На рис. 10
изображен граф, иллюстрирующий рассчитанную центральность.

27
Рисунок 10. Карта центральности ребер графа.

Таким же образом изобразим на графе скорость поездов на каждом из


его ребер (рис. 11).

Рисунок 11. Карта скоростей поездов на графе.

28
2.3. Постановка многокритериальной задачи оптимизации

Как было сказано в предыдущей главе, в работе будет рассмотрена


задача поиска кратчайшего пути. Формально опишем постановку задачи:

𝐹(𝐺, 𝑣𝑠 , 𝑣𝑒 ) = 𝐹(𝑝𝑎𝑡ℎ) → 𝑚𝑖𝑛,

𝐹(𝑝𝑎𝑡ℎ) = ∑𝑒∈𝑝𝑎𝑡ℎ 𝑊(𝑒𝑖𝑗 ), где 𝑊 – соответствие множества ребер


множеству весов ребер.

где 𝐺 - граф железнодорожной транспортной сети, 𝑣𝑠 – вершина начала


пути, 𝑣𝑒 – вершина конца пути, 𝑝𝑎𝑡ℎ – путь от 𝑣𝑠 до 𝑣𝑒 .

Система ограничений:

𝑣𝑠 ≠ 𝑣𝑒

|𝑉(𝐺)| ≠ 0, |𝐸(𝐺)| ≠ 0

𝐺 – связный неориентированный граф

2.4. Разработка алгоритма решения поставленной задачи.

После проведенного анализа, можно сделать вывод, что один из


параметров ребра, а именно максимальная скорость поезда, разделяет граф на
несколько регионов, при этом значения скорости могут значительно
различаться в соседних регионах. Рациональным решением будет исключить
из множества точек графа, те точки скорость передвижения между которыми
заведомо недостаточно велика. Описанный подход называется методом ветвей
и границ, суть которого состоит в разбиении допустимой области значений, в
нашем случае графа железнодорожной сети на подмножества. После
разбиения, получим множество подграфов исходного графа, мощность
которого можно оценить количеством стран входящий в исследуемую
железнодорожную систему.

29
Построим новый граф, вершинами которого будут являться страны,
представленные подграфами, и найдем в нем путь из страны, в которой
находится точка начала в страну с точкой конца пути. Для этого используем
алгоритм Дейкстры поиска кратчайшего пути.

Алгоритм Дейкстры описанный в последнем параграфе предыдущей


главы, использует веса ребер для присвоения меток вершинам. В случае графа
стран, веса ребер рассчитывались как среднее значение средних скоростей в
связанных ребром странах.

После нахождения пути на графе стран, получим граф


железнодорожных путей, входящий в эти страны, и снова воспользуемся
алгоритмом Дейкстры. На этом этапе, необходимо преобразовать вектора
весов ребер в скалярное значение. Для этого будет использоваться функция
𝐹 ∶ ℝ𝑛 → ℝ+ , где 𝑛 – количество параметров ребра. Вид этой функции будет
определять важность того или иного параметра при поиске кратчайшего пути.
После преобразования векторов весов в скаляры, становится возможным
применить алгоритм Дейкстры, результатов котором станет окончательный
путь.

2.5. Методика поиска кратчайшего пути в условиях дефицита


информации.

При недостатке информации, приходится либо строить модель без учета


неизвестных факторов, что негативно скажется на результатах
моделирования, либо прибегать к генерации значений этих факторов. В этой
работе был выбран именно второй способ решения проблемы недостатка
информации.

По косвенным данным, найденным в сторонних источниках, были


сгенерированы отсутствующие значения параметров, которые в дальнейшем
будут использованы при поиске кратчайшего пути.

30
По результатам проведенного визуального анализа, можно сделать вывод
железнодорожные сети европейского континента связаны между собой, и
составляют одну крупномасштабную систему. Не смотря на различные
препятствия функционирования этой системы как единого целого, ее
моделирование может стать более актуальным в обозримом будущем под
действием всеобщего тренда глобализации.

31
3. ПОИСК РЕШЕНИЯ

3.1. Решение поставленной задачи

Для решения поставленной задачи, были реализованы две логические


сущности, реализующие логику вычислений:

• Железнодорожная сеть – объект, хранящий информацию о


станциях и связях между ними.
• Менеджер железнодорожных сетей – объект, хранящий
множество железнодорожных сетей, предоставляющий быстрый
доступ к каждой из них, а также реализующий логику поиска
кратчайшего пути с помощью описанного выше алгоритма.

На рис. 12 13 изображены их интерфейсы. Под интерфейсом здесь


понимается набор функциональных возможностей, предоставляемых
сущностью.

Рисунок 12. Интерфейс железнодорожной сети.

32
Рисунок 13. Интерфейс менеджера железнодорожных сетей.

Каждую из вершин графа представляла точка с географическими


координатами – долгота, широта – представляет объект класса Point (рис. 14),
в котором реализованы методы __hash__ и __eq__, позволяющие сравнивать
вершины при добавлении их в граф. Особенность такой реализации
заключается в том, что она позволяет учитывать ошибки в используемых
данных. Например, если в двух различных полях shape будут находится две
точки с разницей в расстоянии между ними менее 10 м., то это они явно
представляют одну и туже железнодорожную станцию и их можно считать
тождественными.

33
Рисунок 14. Интерфейс сущности Point.

Для удобного выбора точек начала и конца пути, с помощью библиотеки


pygame, было реализовано программное обеспечение позволяющее удобно
выбирать точки отправления и назначения на карте (рис. 15). После выбора
двух точек, ПО проведет расчет пути с помощью описанного ранее алгоритма
и сохранит результат в файл на персональном компьютере пользователя (рис.
17 - 19).

34
Рисунок 15. Разработанное программное обеспечение.

При расчете кратчайших путей в графе стран, и в графе


железнодорожных станции с помощью алгоритма Дейкстры были
использованы две различные функции преобразования вектора весов в скаляр.

На этапе поиска маршрута по странам, наиболее важным является


отсечение стран с низкой максимальной скоростью, поэтому функция
преобразования выглядит следующим образом:

1
𝑓(𝑠𝑝𝑒𝑒𝑑, 𝑑𝑖𝑠𝑡𝑎𝑛𝑐𝑒) = 𝑑𝑖𝑠𝑡𝑎𝑛𝑐𝑒 +
2 ∗ 𝑠𝑝𝑒𝑒𝑑

Скорость в этом случае является более приоритетным параметром,


поэтому имеет больший коэффициент. Функция преобразования на графе
станций использует уже все четыре описанные ранее характеристики:

1 1
𝑓(𝑠𝑝𝑒𝑒𝑑, 𝑑𝑖𝑠𝑡𝑎𝑛𝑐𝑒, 𝑐𝑜𝑠𝑡, 𝑐𝑒𝑛𝑡𝑟𝑎𝑙𝑖𝑡𝑦) = + + 𝑑𝑖𝑠𝑡𝑎𝑛𝑐𝑒 + 𝑐𝑜𝑠𝑡
𝑠𝑝𝑒𝑒𝑑 𝑐𝑒𝑛𝑡𝑟𝑎𝑙𝑖𝑡𝑦

35
Центральность как таковая не влияет на привлекательность того или
иного ребра, но здесь она учтена как характеристика, линейно зависимая от
качества состава. Не смотря на линейную зависимость транспортных
издержек от центральности, они представлены отдельными членами, для учета
случайного возмущения. Так как нашей целью является максимизировать
скорость и качество состава, а алгоритм Дейкстры склонен выбирать ребра с
меньшим весом члены 𝑠𝑝𝑒𝑒𝑑 и 𝑐𝑒𝑛𝑡𝑟𝑎𝑙𝑖𝑡𝑦 находятся в знаменателе.

3.2. Сравнительный анализ эффективности разработанного алгоритма.

Разработанная методика предполагает предварительный расчет


коэффициентов центральности для каждой вершины. В таблице 2
представлено среднее время расчета коэффициентов центральности для
моделей из n-ого количества стран. Не смотря на значительное время
исполнения, эти расчеты выполняются единожды для графа
железнодорожных путей всех стран, входящих в модель.

Кол-во стран в модели Среднее время расчета (сек.)


2 17
3 31
5 79
10 187
20 465

Таблица 2. Время расчета коэффициентов центральности.

Для оценки эффективности алгоритма поиска кратчайшего пути


выберем две случайные вершины в графе – вершину начала и конца пути, а
затем замерим время выполнения алгоритма Дейкстры и разработанного
алгоритма. На рис. 14 видно, что разработанный метод (синий график)

36
примерно в два раза лучше по времени, чем стандартный алгоритм Дейкстры
(оранжевый график), что является хорошим результатом.

Рисунок 16. Графики времени исполнения.

37
ЗАКЛЮЧЕНИЕ

В работе описана методика моделирования крупномасштабных


транспортных систем в условиях недостатка информации, на примере
железнодорожной системы стран Европы и европейской части России.
Разработанная методика позволяет корректно смоделировать транспортную
систему, а также провести поиск кратчайших путей из двух любых точек этой
системы. Разработано гибкое программное обеспечение, позволяющее
визуализировать моделируемую транспортную систему, а также
модифицировать критерии поиска пути.

При поиске кратчайшего пути используется двухэтапный алгоритм,


основанный на алгоритме Дейкстры и методе ветвей и границ. Такой подход
позволяет значительно уменьшить сложность по времени.

Данная работа выполнена мною самостоятельно

«1» июня 2022 г.

38
СПИСОК ИСТОЧНИКОВ

1. Зыков А.А. Основы теории графов. М.:Наука, 1987.


2. Березина Л.Ю. Графы и их применение. М.:Наука, 1971.
3. Толуев Ю.И., Змановская Т.П. Моделирование процессов
перемещения и накопления материальных объектов в
логистических сетях // Логистика: Современные тенденции
развития. V Международная научно-практическая конференция.
20, 21 апреля 2006 г.: Тез. докл. / Отв. ред.: В.С. Лукинский, С.А.
Уваров, Е.А. Королева. – СПб.: СПбГИЭУ, 2006.
4. Толуев Ю.И., Савченко Л.В. Возможности применения
имитационного моделирования в бизнес-логистике. // Логистика:
проблемы и решения, No 4 (5) 2006.
5. Черноруцкий И.Г. Методы оптимизации и принятий решений:
учеб. пособ. / И.Г.Черноруцкий. СПБ.: Лань, 2001.
6. Шенон Р. Имитационное моделирование систем - искусство и
наука М.:Мир, 1978.

39
ПРИЛОЖЕНИЯ

Приложение 1. Скриншоты разработанного ПО.

Рисунок 17. Изображение ПО найденного пути.

Рисунок 18. Изображение ПО железнодорожных станции Франции.

40
Рисунок 19. Изображение ПО железнодорожных станции Германии.

Приложение 2. Программный код разработанного ПО.

railwaynet.py

from functools import reduce, lru_cache


from geograph import GeoGraph, Point
from numpy.random import default_rng
from matplotlib import pyplot as plt
from dataclasses import dataclass
from rich.console import Console
from scipy.stats import norm
from statistics import mean
from tqdm import tqdm

import networkx as nx
import pandas as pd
import matplotlib
import contextlib
import pycountry
import pickle
import random
import bz2

matplotlib.use('Qt5Agg')

# region Constants

NORM_RANDOM = lambda : norm.rvs(size=1, random_state=default_rng())[0]

41
COLORS = [
'crimson', 'cyan',
'darkblue', 'darkcyan', 'darkgoldenrod',
'darkgray', 'darkgreen', 'darkkhaki',
'darkmagenta', 'darkolivegreen', 'darkorange',
'darkorchid', 'darkred', 'darksalmon',
'darkseagreen', 'darkslateblue', 'darkslategray',
'darkturquoise', 'darkviolet', 'deeppink',
'deepskyblue', 'dimgray', 'dodgerblue',
'firebrick', 'floralwhite', 'forestgreen',
'fuchsia', 'gainsboro', 'ghostwhite',
'gold', 'goldenrod', 'gray',
'green', 'greenyellow', 'honeydew',
'hotpink', 'indianred', 'indigo',
'ivory', 'khaki', 'lavender',
'lavenderblush', 'lawngreen', 'lemonchiffon',
'lightblue', 'lightcoral', 'lightcyan',
'lightgoldenrodyellow', 'lightgreen', 'lightgray',
'lightpink', 'lightsalmon', 'lightseagreen',
'lightskyblue', 'lightslategray', 'lightsteelblue',
'lightyellow', 'lime', 'limegreen',
'linen', 'magenta', 'maroon',
'mediumaquamarine', 'mediumblue', 'mediumorchid',
'mediumpurple', 'mediumseagreen', 'mediumslateblue',
'mediumspringgreen', 'mediumturquoise', 'mediumvioletred',
'midnightblue', 'mintcream', 'mistyrose',
'moccasin', 'navajowhite', 'navy',
'oldlace', 'olive', 'olivedrab',
'orange', 'orangered', 'orchid',
'palegoldenrod', 'palegreen', 'paleturquoise',
'palevioletred', 'papayawhip', 'peachpuff',
'peru', 'pink', 'plum',
'powderblue', 'purple', 'red',
'rosybrown', 'royalblue', 'saddlebrown',
'salmon', 'sandybrown', 'seagreen',
'seashell', 'sienna', 'silver',
'skyblue', 'slateblue', 'slategray',
'snow', 'springgreen', 'steelblue',
'tan', 'teal', 'thistle',
'tomato', 'turquoise', 'violet',
'wheat', 'white', 'whitesmoke',
'yellow', 'yellowgreen'
]

CONSOLE = Console()

CALCULATING_COMPONENTS_MSG = "Calculating components"


CALCULATING_GRAPHS_MSG = " Calculating graphs"
COMBINING_GRAPHS_MSG = " Combinig graphs"
CALCULATING_CENTRALITY_MSG = "Calculating centrality"

PROGRESS_BAR_WIDTH = 100

# endregion

# region Types

@dataclass
class RailwayNetInfo:

42
nodes: int
edges: int
components: int
biggest_component_part: float

class RailwayNet(GeoGraph):

# region Construction

def __init__(self, graph_data: pd.DataFrame = None, countries_data:


pd.DataFrame = None, iso3: str = None):
super(RailwayNet, self).__init__()

self.countries = set()

if graph_data is not None:


if iso3 is not None:
self.countries.add(iso3)

data_filtered = graph_data if iso3 is None else


graph_data[graph_data.iso3 == iso3]
for trail in data_filtered['shape']:
lat, lon = RailwayNet.__get_coordinates_from_string(trail)
self.add_node(Point(lon[0], lat[0]), iso3=iso3)
for i in range(len(lat) - 1):
b = Point(lon[i + 1], lat[i + 1])
a = Point(lon[i], lat[i])
self.add_node(b, iso3=iso3)
if a.lat != b.lat or a.lon != b.lon:
self.add_edge(
a,
b,
distance=a.distance(b),

centrality=a.distance(countries_data[iso3]['capital']),
speed=(80 if countries_data is None else
countries_data[iso3]['speed'])+ NORM_RANDOM() * 5,
iso3=iso3
)

# endregion

# region PublicMethods

@lru_cache(maxsize=None)
def get_biggest_component(self):
return self.subgraph(max(nx.connected_components(self), key=len))

def get_points_dataframe(self):
points_dataframe = pd.DataFrame(
{
'lat': [node.coord_reverse[0] for node in self.nodes],
'lon': [node.coord_reverse[1] for node in self.nodes],
'iso3': [self.nodes[node]['iso3'] for node in self.nodes]
}
)
return points_dataframe

def draw_plot_by_country(self, size: tuple[int, int] = (20, 10, ), ):

43
plt.figure(figsize=size)
edge_color=[COLORS[ord(self[u][v]['iso3'][0]) % len(COLORS)] for u, v in
self.edges]
self.draw(edge_color=edge_color, node_size=0)

def draw_plot_by_component(self, size: tuple[int, int] = (20, 10)):


plt.figure(figsize=size)
components = sorted(nx.connected_components(self), key=len,
reverse=True)[:20]
for index, component in enumerate(tqdm(components,
desc=CALCULATING_COMPONENTS_MSG)):
self.subgraph(component).draw(edge_color=COLORS[index %
(len(COLORS))], node_size=0)

def draw_plot_by_attribute(self, attr: str, size: tuple[int, int] = (20, 10)):

def green2red(ratio: float) -> tuple[float, float, float]:


return (ratio, 1 - ratio, 0)

plt.figure(figsize=size)
attr_list = [self[u][v][attr] for u,v in self.edges]
min_attr = min(attr_list)
ratio_derivative = max(attr_list) - min_attr
self.draw(edge_color=[green2red((attr - min_attr)/ratio_derivative) for
attr in attr_list], node_size=0)

def describe(self, verbose=True) -> RailwayNetInfo:


nodes = nx.number_of_nodes(self)
edges = nx.number_of_edges(self)
components = nx.number_connected_components(self)
biggest_component_part = \
nx.number_of_nodes(self.get_biggest_component()) /
nx.number_of_nodes(self)

if verbose:
res = "\n"
res += f" nodes: {nodes}\n"
res += f" edges: {edges}\n"
res += f" components: {components}\n"
res += f" biggest component part: {biggest_component_part:.6}\n"
print(res)

return RailwayNetInfo(
nodes=nodes,
edges=edges,
components=components,
biggest_component_part=biggest_component_part
)

# endregion

# region ServiceMethods

@staticmethod
def __get_coordinates_from_string(coordinates_string: str) ->
tuple[list[float], list[float]]:
""" function which formats (lat, long) data nicely """

tuple_string = coordinates_string[15:]

44
coordinate_strings_list = tuple_string.lstrip("( ").rstrip(")
").split(',')
lon = []
lat = []

for lon_lat_pair in coordinate_strings_list:


lon.append(float(lon_lat_pair.strip().split()[0].strip("()")))
lat.append(float(lon_lat_pair.strip().split()[1].strip("()")))

return lon, lat

# endregion

class PathEdgePoint:
def __init__(self, node: Point, iso3: str):
self.node = node
self.iso3 = iso3

def __eq__(self, other):


if other is None:
return False
return self.point == other.point and other.iso3 == other.iso3

class CountryNet(GeoGraph):

# region Construction

def __init__(self, countries_data: dict = None):


super(CountryNet, self).__init__()

if countries_data is not None:


for country, country_attributes in countries_data.items():
self.add_node(country_attributes['capital'], iso3 = country)
for neighbour in country_attributes['neighbours']:
if neighbour == country:
continue
self.add_edge(
country_attributes['capital'],
countries_data[neighbour]['capital'],

distance=country_attributes['capital'].distance(countries_data[neighbour]['capital
']),
speed=(country_attributes['speed'] +
countries_data[neighbour]['speed']) / 2,
)

# endregion

class RailwayNetManager(dict):

# region Constants

CACHED_LIST_OF_NETS_PATH = "./cached/graphs_list.bz2"
CACHED_FULL_GRAPH_PATH = "./cached/graph_full.bz2"

# endregion

# region Construction

45
def __init__(self, graph_data: pd.DataFrame, countries_data: pd.DataFrame):
self.graph_data = graph_data
self.countries_data =
RailwayNetManager.__countries_dataframe2dict(countries_data)

self.start_node = None
self.finish_node = None

# sort countries by amount of railways in it


self.countries_sorted =
self.graph_data.iso3.value_counts().keys().to_list()

# try to load cached list of nets


# if not found, calculate
# then init dict with graph values
railway_nets =
try_load_cached_file(RailwayNetManager.CACHED_LIST_OF_NETS_PATH)
if railway_nets is None:
railway_nets = [RailwayNet(
graph_data=self.graph_data,
countries_data=self.countries_data,
iso3=iso3
) for iso3 in tqdm(self.countries_sorted,
desc=CALCULATING_GRAPHS_MSG)]

save_file_to_cache(railway_nets,
RailwayNetManager.CACHED_LIST_OF_NETS_PATH)
super(RailwayNetManager, self).__init__(zip(self.countries_sorted,
railway_nets))
self.full_graph = None
self.full_graph = self.__get_full()

for country in self.countries_data:


self.countries_data[country]['neighbours'] = set()
self.countries_data[country]['neighbours'].add(country)

for edge in self.full_graph.edges:


a_country = self.full_graph.nodes[edge[0]]['iso3']
b_country = self.full_graph.nodes[edge[1]]['iso3']
if a_country != b_country:
self.countries_data[a_country]['neighbours'].add(b_country)

self.countries_graph = CountryNet(self.countries_data)

# endregion

# region PublicMethods

def describe(self) -> pd.DataFrame:


countries = []
nodes = []
edges = []
components = []
biggest_component_part = []
for key, value in self.items():
info = value.describe(False)
countries.append(key)
nodes.append(info.nodes)
edges.append(info.edges)

46
components.append(info.components)
biggest_component_part.append(info.biggest_component_part)
description = pd.DataFrame(
{
"country" : countries,
"nodes" : nodes,
"edges" : edges,
"components" : components,
"biggest_component_part" : biggest_component_part
}
)
with open("railwaynet_description.csv", "w") as f:
description.to_csv(f)

return description

def get_random(self):
countries_number = random.randrange(1, len(self.countries_sorted + 1))
countries_list = []
while len(countries_list) != countries_number:
country_chosen = random.choice(self.countries_sorted)
if country_chosen not in countries_list:
countries_list.append(country_chosen)

return self.get_nets(countries_list)

def get_net(self, iso3: str) -> RailwayNet | None:


if iso3 in self.countries_sorted:
if self[iso3] is None:
self[iso3] = RailwayNet(
self.graph_data,
countries_data=self.countries_data,
iso3=iso3
)
return self[iso3]
return None

def get_nets(self, iso3_lst: list[str], recalculate_centrality: bool = True) -


> RailwayNet | None:
res = RailwayNet()

def compose(G, H):


C = nx.compose(G, H)
C.countries = G.countries.union(H.countries)
return C

res = reduce(compose, tqdm([self.get_net(iso3) for iso3 in iso3_lst if


iso3 in self.countries_sorted], desc=COMBINING_GRAPHS_MSG), res)
if nx.number_of_nodes(res) <= 0:
return None

if recalculate_centrality:
self.__calculate_centrality(res)
return res

def save_node(self, point: tuple[float, float], iso3: str) -> bool:


for node in self.full_graph.nodes:
if node.coord == point:
if self.start_node is None:

47
self.start_node = PathEdgePoint(node, iso3)
return True
elif self.finish_node is None:
self.finish_node = PathEdgePoint(node, iso3)
return True
return False
return False

def find_path(self, o_paths: list) -> None:

def country_func(u,v,e_attrs):
return e_attrs['distance'] + 2 * e_attrs['speed']

def func(u,v,e_attrs):
return e_attrs['distance'] + 2 * e_attrs['speed'] + 0.5 *
e_attrs['centrality']

if self.start_node.iso3 == self.finish_node.iso3:
o_paths[1] = nx.dijkstra_path(
self.full_graph.get_biggest_component(),
self.start_node.node,
self.finish_node.node,
func)

else:
frm = None
to = None
for n in self.countries_graph.nodes:
if self.countries_graph.nodes[n]['iso3'] == self.start_node.iso3:
frm = n
if self.countries_graph.nodes[n]['iso3'] == self.finish_node.iso3:
to = n
o_paths[0] = cpath = nx.dijkstra_path(self.countries_graph, frm, to,
country_func)
countries_in_path = []
for p in cpath:
for n in self.countries_graph.nodes:
if n == p:

countries_in_path.append(self.countries_graph.nodes[n]['iso3'])
g = self.get_nets(countries_in_path)
o_paths[1] = nx.dijkstra_path(
g,
self.start_node.node,
self.finish_node.node,
func)

# endregion

# region ServiceMethods

def __get_full(self) -> RailwayNet:


# if full graph is initiated
if self.full_graph is not None:
return self.full_graph

# try to load graph of all nets


# if not found, calculate

48
full_graph =
try_load_cached_file(RailwayNetManager.CACHED_FULL_GRAPH_PATH)
if full_graph is None:
full_graph = self.get_nets(self.countries_sorted,
recalculate_centrality=False)
save_file_to_cache(full_graph,
RailwayNetManager.CACHED_FULL_GRAPH_PATH)
return full_graph

def __calculate_centrality(self, g: RailwayNet) -> None:


for edge in tqdm(g.edges, desc=CALCULATING_CENTRALITY_MSG):
g.edges[edge]['centrality'] = \
mean(
(edge[0].distance(self.countries_data[neighbour]['capital']) \
for neighbour in \

self.countries_data[g.edges[edge]['iso3']]['neighbours']))
g.edges[edge]['cost'] = 1 / g.edges[edge]['centrality'] +
NORM_RANDOM()

@staticmethod
def __countries_dataframe2dict(countries_data: pd.DataFrame) -> dict:
countries_dict = dict()
for iso3 in countries_data.iso3:
country = countries_data[countries_data.iso3 == iso3].iloc[0]
countries_dict[iso3] = {
'speed' : country.MaximumTrainSpeed,
'capital' : Point(country.CapitalLatitude,
country.CapitalLongitude)
}
return countries_dict

# endregion

# endregion

# region Functions

def default_setup() -> RailwayNetManager:


# manage graph data
data_path = "./data/trains.csv"
data = pd.read_csv(data_path, sep=',', dtype=str)[["iso3", "shape"]]

# manage countries data


capitals_data_path = "./data/country_capitals.csv"
capitals_data = pd.read_csv(capitals_data_path,
sep=',').dropna()[['CountryCode', 'CapitalLatitude', 'CapitalLongitude']]
capitals_data['iso3'] = capitals_data.CountryCode.apply(lambda c :
pycountry.countries.get(alpha_2 = c).alpha_3 if pycountry.countries.get(alpha_2 =
c) is not None else None)
capitals_data = capitals_data.drop(columns=['CountryCode'])

speed_data_path="./data/train_speed.csv"
speed_data = pd.read_csv(speed_data_path, sep=',')
speed_data['iso3'] = speed_data.CountryName.apply(lambda name:
pycountry.countries.search_fuzzy(name)[0].alpha_3)
speed_data = speed_data.drop(columns=['CountryName'])

# merge countries

49
countries_data = speed_data.merge(capitals_data)

# synchronize graph data by available countries data


data = data[data.iso3.isin(countries_data.iso3)]

return RailwayNetManager(graph_data=data, countries_data=countries_data),


data, countries_data

def try_load_cached_file(path: str):


data = None
try:
with CONSOLE.status(f"Loading {path} file"),
contextlib.closing(bz2.BZ2File(path, 'rb')) as f:
print(f"File '{path}' found")
data = pickle.load(f)
except FileNotFoundError as e:
print(e)

return data

def save_file_to_cache(obj, path: str):


with contextlib.closing(bz2.BZ2File(path, 'wb')) as f:
pickle.dump(obj, f)

# endregion

geograph.py

import networkx as nx

from matplotlib import pyplot as plt


from geopy import distance

class Color:

# region construction

def __init__(self, r: float, g: float, b: float):


self.r = r
self.g = g
self.b = b
self.rgb = (self.r, self.g, self.b)

# endregion

class Point:

# region Construction

def __init__(self, lat: float, lon: float):


self.lat = lat
self.lon = lon
self.coord = (lon, lat)
self.coord_reverse = (lat, lon)

# endregion

50
# region PublicMethods

def distance(self, other) -> float:


return distance.distance(self.coord_reverse, other.coord_reverse).km

# endregion

# region OverloadMethods

def __eq__(self, other):


return self.lat == other.lat and self.lon == other.lon

def __hash__(self):
return hash(self.coord)

# endregion

class GeoGraph(nx.Graph):

# region Construction

def __init__(self):
super(GeoGraph, self).__init__()

# endregion

# region PublicMethods

def draw(self, edge_color=None, node_color=None, width=None, node_size=None):


nx.draw(self,
edgelist=self.edges,
edge_color='black' if edge_color is None else edge_color,
width=1 if width is None else width,
nodelist=self.nodes,
node_color='red' if node_color is None else node_color,
node_size=1 if node_size is None else node_size,
pos=dict(zip(self.nodes, [node.coord for node in self.nodes]))
)

# endregion

editor.py

from scipy.spatial import KDTree


from graphrenderer import GraphRenderer
from railwaynet import RailwayNetManager, COLORS
from guirenderer import GUIRenderer
from threading import Thread
import pygame as pg

class Editor:
def __init__(self, railway_net_manager: RailwayNetManager):

pg.init()

51
# init application screen
self.size = self.w, self.h = (1900, 900)
self.screen = pg.display.set_mode(self.size)
self.screen.fill((255, 240, 250))
pg.display.flip()

# set utility attributes


self.running = False
self.current_country = "full"
self.search_range = 2
self.current_paths = [None, None]

# set graph data


self.railway_net_manager = railway_net_manager

# set renderers
self.gui_surface = pg.Surface((1900, 50))
self.graph_surface = pg.Surface((1900, 850))
self.gui_renderer = GUIRenderer(self.gui_surface,
self.railway_net_manager.countries_sorted)
self.graph_renderer = GraphRenderer(
self.graph_surface,
self.railway_net_manager.full_graph.get_biggest_component(),
COLORS,
self.search_range
)

def find_path_routine(self):
self.railway_net_manager.find_path(self.current_paths)
self.graph_renderer.render()
animate = False
if self.current_paths[1] is not None:
if self.current_paths[0] is not None:
self.current_country = "full"

self.graph_renderer.update_graph(self.railway_net_manager.full_graph.get_biggest_c
omponent())
self.graph_renderer.render(animate=True)

self.graph_renderer.update_path(self.current_paths[1])
self.graph_renderer.render()

def manage_guirenderer_event(self, event: pg.event):


result = self.gui_renderer.check_event(event)
if result is not None:
if result == "reset":
self.railway_net_manager.start_node = None
self.railway_net_manager.finish_node = None
self.graph_renderer.path = None
self.graph_renderer.path_points_data = None
else:
self.current_country = result
if result == "full":

self.graph_renderer.update_graph(self.railway_net_manager.full_graph.get_biggest_c
omponent())
else:

52
self.graph_renderer.update_graph(self.railway_net_manager.get_net(result))
self.graph_renderer.render()

def manage_graphrenderer_event(self, event: pg.event):


result = self.graph_renderer.check_event(event)
if result is not None:
self.railway_net_manager.save_node(result, self.current_country)
self.graph_renderer.render()
if self.railway_net_manager.start_node is not None and \
self.railway_net_manager.finish_node is not None:
t = Thread(target=self.find_path_routine)
t.start()

def run(self):
self.graph_renderer.render(animate=True)
self.path = None

self.running = True
while self.running:
for event in pg.event.get():
if event.type == pg.QUIT:
self.running = False
if event.type == pg.KEYDOWN:
if event.key == pg.K_ESCAPE:
self.running = False

self.manage_guirenderer_event(event)

if self.current_country != "full":
self.manage_graphrenderer_event(event)

self.gui_renderer.render()

self.screen.blit(self.gui_surface, (0,0))
self.screen.blit(self.graph_surface, (0,50))

pg.draw.circle(self.screen, 'blue', pg.mouse.get_pos(),


self.search_range, 1)

pg.display.update()

graphrenderer.py

from railwaynet import *


from scipy.spatial import cKDTree
from threading import Thread

import pygame as pg

class GraphRenderer:

53
# region Construction

def __init__(
self,
surface: pg.Surface,
graph: RailwayNet,
colors: list[str],
search_range: int = 2,
bg_color: tuple[int, int, int] = (0,0,0)
):
self.surface = surface
self.size = self.w, self.h = self.surface.get_size()

self.bg_color = bg_color
self.colors = colors

self.graph = graph
self.points_data = None

self.path = None
self.path_points_data = None

self.search_tree = None
self.search_range = search_range

self.update_points_positions()
self.update_search_tree()

# endregion

# region PublicMethods

def update_points_positions(self) -> None:


self.points_data = self.graph.get_points_dataframe()

self.vertical_bounds = self.points_data.lat.max(),
self.points_data.lat.min()
self.horizontal_bounds = self.points_data.lon.min(),
self.points_data.lon.max()
self.vertical_span = self.vertical_bounds[1] - self.vertical_bounds[0]
self.horizontal_span = self.horizontal_bounds[1] -
self.horizontal_bounds[0]

self.points_data['x'] = self.points_data.lon.apply(
lambda lon : self.world2local(lon)
)
self.points_data['y'] = self.points_data.lat.apply(
lambda lat : self.world2local(lat, horizontal=False)
)
self.points_data['color'] = self.points_data.iso3.apply(
lambda iso3: self.colors[ord(iso3[0]) % len(self.colors)]
)
self.points_data['radius'] = [1] * self.points_data.shape[0]

def update_search_tree(self) -> None:


self.search_tree = cKDTree(
[(self.points_data.x.iloc[i], self.points_data.y.iloc[i]) for i in
self.points_data.index]
)

54
def update_graph(self, graph: RailwayNet) -> None:
self.graph = graph
self.update_points_positions()
self.update_search_tree()
if self.path is not None:
self.update_path_points_positions()

def update_path_points_positions(self):
if self.path_points_data is None:
self.path_points_data = pd.DataFrame()
self.path_points_data['x'] = [self.world2local(point.coord[0]) for point
in self.path]
self.path_points_data['y'] = [self.world2local(point.coord[1],
horizontal=False) for point in self.path]

def update_path(self, path: list[Point]):


self.path = path
self.update_path_points_positions()

def check_event(self, event: pg.event) -> tuple[float, float] | None:


if event.type == pg.MOUSEBUTTONDOWN:
mouse_coord = pg.mouse.get_pos()
mouse_coord = (mouse_coord[0], mouse_coord[1] - 50)
found_points = self.search_tree.query_ball_point(mouse_coord,
r=self.search_range)
if found_points:
point_index = found_points[0]
self.points_data.loc[:, ('color')].iloc[point_index] = 'red'
self.points_data.loc[:, ('radius')].iloc[point_index] = 3
return (
self.points_data.lon.iloc[point_index],
self.points_data.lat.iloc[point_index]
)

def render(self, animate=False) -> None:


if animate:
t = Thread(target=self.render_internal)
t.start()
else:
self.render_internal()

def render_internal(self) -> None:


self.surface.fill(self.bg_color)
for i in self.points_data.index:
pg.draw.rect(
self.surface,
self.points_data.color.iloc[i],
pg.Rect(
(self.points_data.x.iloc[i], self.points_data.y.iloc[i]),
(self.points_data.radius.iloc[i],
self.points_data.radius.iloc[i])),
self.points_data.radius.iloc[i]
)

if self.path_points_data is not None:


for i in self.path_points_data.index[:-1]:
pg.draw.line(
self.surface,

55
'red',
(self.path_points_data.x.iloc[i],
self.path_points_data.y.iloc[i]),
(self.path_points_data.x.iloc[i + 1],
self.path_points_data.y.iloc[i + 1]),
2
)

# endregion

# region ServiceMethods

def world2local(self, value: float, horizontal: bool = True):


if horizontal:
return (value - self.horizontal_bounds[0]) / self.horizontal_span *
self.w
return (value - self.vertical_bounds[0]) / self.vertical_span * self.h

# endregion

guirenderer.py

import pygame as pg
import pygame_gui as pgg

class GUIRenderer:

# region Construction

def __init__(self, surface: pg.Surface, countries: list[str]):


self.surface = surface
self.size = self.w, self.h = self.surface.get_size()

self.countries = countries

self.manager = pgg.UIManager(self.size, theme_path='theme.json')


self.clock = pg.time.Clock()
self.time_delta = self.clock.tick(60) / 1000.0

self.country_buttons = dict()
button_width = self.w / (len(self.countries) + 2) - 1
button_height = self.h
self.country_buttons = dict()

self.reset_button = pgg.elements.UIButton(
relative_rect=pg.Rect((0, 0), (button_width, button_height)),
text="R",
manager=self.manager)

self.country_buttons["full"] = pgg.elements.UIButton(
relative_rect=pg.Rect((button_width, 0), (button_width,
button_height)),
text="full",
manager=self.manager)

56
for i, country in enumerate(self.countries):
self.country_buttons[country] = \
pgg.elements.UIButton(
relative_rect=pg.Rect(((i + 2) * button_width, 0),
(button_width, button_height)),
text=country,
manager=self.manager)

# endregion

# region PublicMethods

def check_event(self, event: pg.event) -> str:


result = None
if event.type == pgg.UI_BUTTON_PRESSED:
for country, button in self.country_buttons.items():
if event.ui_element == button:
result = country
if event.ui_element == self.reset_button:
result = "reset"
self.manager.process_events(event)
return result

def render(self) -> None:


self.manager.update(self.time_delta)
self.manager.draw_ui(self.surface)
self.time_delta = self.clock.tick(60) / 1000.0

# endregion
main.py

from railwaynet import *


from editor import *

# region Main

def main() -> None:


manager, _, _ = default_setup()

editor = Editor(manager)
editor.run()

# endregion

if __name__ == "__main__":
main()

57

Вам также может понравиться