Найти тему

Решение региональной задачи по #логистике ТМЦ с помощью #ИИ (#ChatGPT). Часть 1.

Оглавление

Всем привет!

Это моя первая статья на ресурсе.

Обо мне: Владимир, действующий сотрудник одной из компаний в сфере транспортного инжиниринга всеми видами транспорта. Стаж в сфере: с июня 2015 года, 9 лет. Начинал с должности менеджера по логистике, затем пошагово вырос до роли руководителя отдела продаж и директора филиала.

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

Лирическое отступление

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

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

Задача

Обратился ко мне мой старый товарищ с просьбой помочь в планировании логистики, он регулярно занимается производством ЖБИ изделий и требуется их доставка, а также выгрузка на территории Грузополучателя.

Известно:

  • Всего 1115 точек выгрузки по Татарстану.
  • Завод отгрузки в г. Нижнекамске.
  • Груз: ЖБИ изделия, массой по 500 кг, размером 0,5 на 0,5 на 0,2 метра.
  • Срок выполнения задачи: 2 месяца.
Места выгрузки
Места выгрузки

Предварительный логический разбор задачи:

Республика Татарстан является одним из Субъектов Россиийской Федерации, население которого составляет 4 млн. человек (2024).

Понимая очевидную взаимосвязь (где люди - там и экономика, а где экономика там и склады, а где склады там и грузовая техника), и, как следствие, необходимость выбора нескольких точек складирования, были выбраны 2 основных города регионального значения - г. Казань и г. Набережные Челны, совокупным населением 2 млн. человек (50% от общего населения РТ).

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

В тоже время, г. Казань находится в 225 км. от завода производителя, что говорит о необходимости создания склада отгрузки, так как возврат пустого грузовика займёт у нас лишнее время (и деньги).

Увеличенная карта
Увеличенная карта

Первичный технический анализ задачи.

К счастью, мой дорогой друг предоставил мне данные о точках выгрузки в Google Maps, отчего мне лишь осталось экспортировать данные карты в формате .kmz, затем предоставить файл ChatGPT.

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

Работаем с координатами:

-3

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

Затем, нам нужно распределить, какие точки выгрузки ближе к точкам погрузки (Казани и Нижнекамску).

Прим.: к сожалению, у меня нет опыта работы с Яндекс Карты API либо аналогичными навигационными системами, для корректного расчёта расстояний по автодорогам, в том числе с учётом пробок, потому полагаюсь на возможности ChatGPT в части расчётов координат "в лоб", то есть по-ветру, прямое расстояние от точки до точки, без учёта дорог и пробок. В этом подходе есть и минусы - страдает объективность расчётов, но это в любом случае лучше чем ничего.

-4

ChatGPT определил, что для Казани наиболее приближены 520 точек, оставшиеся 595 точек наименее удалены к Нижнекамску.

Добавляем технических деталей

Известно, что завод в г. Нижнекамске:

  • работает с 8-00 до 17-00 5 дней в неделю
  • возможная погрузка на ТС до 80 шт. в течение рабочего дня
  • производительность завода: 50 шт. в течение рабочего дня, текущий склад полон на 600 шт.

Аналогичные требования предъявим и к складу в г. Казани, для упрощения планирования.

Что известно о пунктах выгрузки:

  • также работают с 08-00 до 17-00, 5 дней в неделю.
  • выгрузка требуется силами перевозчика, то есть нам нужен грузовик с КМУ
  • на 1 пункт выгрузки - 1 единица ЖБИ
  • средняя скорость выгрузки, с учётом прошлого опыта, составляет от 15 до 30 минут.

Используемые ТС:

  • к работе нами выбраны грузовики с КМУ, тяговой грузоподъемностью 10 тн, КМУ устройство с грузоподъемностью 1,5 тонны.

Все эти параметры, разумеется, сообщаем для ChatGPT.

-5

К сожалению, ChatGPT имеет ограничения среды по обработке ваших данных, потому в моем случае мне было предложено использовать либо локальный Python либо виртуальные сервисы вроде Google Collab, и предоставлен исполнительный код для загрузки моих данных и получения готового файла.

Я остановился на последнем варианте.

import pandas as pd
from geopy.distance import geodesic
from google.colab import files
from datetime import datetime, timedelta
import time

# Загрузка нового файла
uploaded = files.upload()

# Убедитесь, что имя файла соответствует загруженному
file_path = 'coordinates_baza_retry.xlsx'
df = pd.read_excel(file_path)

# Координаты базы
loading_point_baza = (55.78991698207294, 49.14626903715072)

# Константы
loading_time_per_batch = 4 * 60 # 4 часа в минутах
units_per_batch = 20
average_unloading_time_per_stop = 30 # 30 минут на выгрузку
average_speed_kmh = 60 # км/ч
working_hours_per_day = 9 * 60 # рабочий день в минутах
end_of_working_day = 17 * 60 # 17:00 в минутах с начала дня (8:00)
start_of_working_day = 8 * 60 # 8:00 в минутах с начала дня (0:00)
latest_return_time = 12 * 60 # 12:00 в минутах с начала дня (0:00)

# Функция для вычисления времени пути
def compute_travel_time(distance_km, speed_kmh):
return (distance_km / speed_kmh) * 60 # Перевод в минуты

# Функция для конвертации минут в формат времени (24-часовой формат)
def convert_minutes_to_time(minutes):
return (datetime(2024, 7, 12, 0, 0) + timedelta(minutes=minutes)).strftime('%H:%M')

# Функция для планирования маршрута
def plan_routes(df, start_point, units_per_batch, unloading_time, speed, loading_time, start_of_day, end_of_day, latest_return_time):
routes = []
distances_per_day = []
remaining_df = df.copy()
current_day = 1
total_units = 0
current_time = start_of_day
current_point = start_point
total_distance = 0
total_time = 0

# Начало первого дня с погрузкой
start_time = current_time
current_time += loading_time
routes.append({
'Day': current_day,
'Name': 'База / Погрузка на Базе',
'Coordinates': f"{loading_point_baza[0]},{loading_point_baza[1]}",
'Distance from Previous (km)': 0,
'Start Time': convert_minutes_to_time(start_time),
'End Time': convert_minutes_to_time(current_time),
'Total Distance (km)': total_distance,
'Total Time (min)': total_time + loading_time
})
total_time += loading_time

while not remaining_df.empty:
day_route = []
day_distances = []

while total_units < units_per_batch and current_time < end_of_working_day and not remaining_df.empty:
remaining_df['Distance'] = remaining_df['Coordinates'].apply(
lambda x: geodesic(current_point, tuple(map(float, x.split(',')))).kilometers
)
nearest_point_idx = remaining_df['Distance'].idxmin()
nearest_point = remaining_df.loc[nearest_point_idx]

travel_time = compute_travel_time(nearest_point['Distance'], speed)
if current_time + travel_time + unloading_time > end_of_working_day:
break

start_time = current_time
current_time += travel_time
end_time = current_time + unloading_time

day_route.append({
'Day': current_day,
'Name': nearest_point['Name'],
'Coordinates': nearest_point['Coordinates'],
'Distance from Previous (km)': nearest_point['Distance'],
'Start Time': convert_minutes_to_time(start_time),
'End Time': convert_minutes_to_time(end_time),
'Total Distance (km)': total_distance + nearest_point['Distance'],
'Total Time (min)': total_time + travel_time + unloading_time
})

day_distances.append(nearest_point['Distance'])
current_point = tuple(map(float, nearest_point['Coordinates'].split(',')))
remaining_df = remaining_df.drop(nearest_point_idx)
total_units += 1
total_distance += nearest_point['Distance']
total_time += travel_time + unloading_time
current_time = end_time

routes.extend(day_route)
distances_per_day.extend(day_distances)

if total_units >= units_per_batch or current_time >= end_of_working_day:
return_time = compute_travel_time(geodesic(current_point, start_point).kilometers, speed)
current_time += return_time

routes.append({
'Day': current_day,
'Name': 'База / Возвращение пустого грузовика на Базу',
'Coordinates': f"{loading_point_baza[0]},{loading_point_baza[1]}",
'Distance from Previous (km)': return_time,
'Start Time': convert_minutes_to_time(current_time - return_time),
'End Time': convert_minutes_to_time(current_time),
'Total Distance (km)': total_distance + return_time,
'Total Time (min)': total_time + return_time
})
distances_per_day.append(return_time)
current_point = start_point
total_units = 0

if current_time > end_of_working_day or current_time > latest_return_time:
current_day += 1
current_time = start_of_working_day
start_time = current_time
current_time += loading_time
routes.append({
'Day': current_day,
'Name': 'База / Погрузка на Базе',
'Coordinates': f"{loading_point_baza[0]},{loading_point_baza[1]}",
'Distance from Previous (km)': 0,
'Start Time': convert_minutes_to_time(start_time),
'End Time': convert_minutes_to_time(current_time),
'Total Distance (km)': 0,
'Total Time (min)': loading_time
})
total_distance = 0
total_time = loading_time
else:
current_time += loading_time

current_day += 1
current_time = start_of_working_day

return routes, distances_per_day

# Планирование маршрутов
start_time = time.time()

routes, distances_per_day = plan_routes(
df, loading_point_baza, units_per_batch,
average_unloading_time_per_stop, average_speed_kmh,
loading_time_per_batch, start_of_working_day, end_of_working_day, latest_return_time
)

end_time = time.time()
print(f"Time taken: {end_time - start_time} seconds")

# Создание итогового списка с дополнительными строками
final_route_summary = []

for i, stop in enumerate(routes):
final_route_summary.append({
'Day': stop['Day'],
'Name': stop['Name'],
'Coordinates': stop['Coordinates'],
'Distance from Previous (km)': stop['Distance from Previous (km)'],
'Distance to Next (km)': distances_per_day[i+1] if i < len(distances_per_day) - 1 else 0,
'Start Time': stop['Start Time'],
'End Time': stop['End Time'],
'Total Distance (km)': stop['Total Distance (km)'],
'Total Time (min)': stop['Total Time (min)']
})

final_route_summary_df = pd.DataFrame(final_route_summary)

# Сохранение результатов в Excel файл
output_file_path_final_baza_xlsx = 'final_route_summary_baza_with_loading_return_with_times.xlsx'
final_route_summary_df.to_excel(output_file_path_final_baza_xlsx, index=False)

# Скачать результат
files.download(output_file_path_final_baza_xlsx)
-6

Вторичный технический анализ

Разумеется, в последствии нужно будет добавить ему деталей.

К примеру:

  • если грузовик приехал на одну из точек выгрузки имея в запасе ещё ЖБИ, то он не возвращается на пункт погрузки раньше чем не выполнит выгрузку всех ЖБИ изделий и остаётся ночевать в грузовике
  • если грузовик вернулся на пункт отгрузки позже чем в 12-00, то его уже не успеют погрузить в этот день, значит он будет погружен с 08-00 следующего рабочего дня
  • необходимость отразить расстояние пробега после погрузки до этапа последней выгрузки и пустого хода до погрузки
  • и так далее, добавляя еще и ещё технических деталей, вплоть до того что вы работаете в 24 часовом формате, а не в 12 часовом.

Предварительный итог

На выходе, после обработки кода через Google Collab, я получил следующий файл:

-7

Здесь предварительно описан график движения грузовика относительно Завода в г. Нижнекамске в первые 2 дня, с учетом погрузки в первый день и возврата пустым только на следующий день в 13:41.

Разумеется, есть погрешность в расстоянии и затраченном времени, так как мы используем прямые расчёты расстояний по координатам без учёта автодорог и пробок, но тем не менее, это позволяет определить особо важные детали, к примеру:

-8

Разумеется, есть погрешность в расстоянии и затраченном времени, так как мы используем прямые расчёты расстояний по координатам без учёта автодорог и пробок, но тем не менее, это позволяет определить особо важные детали, к примеру:

  • Даже при таком грубом расчёте расстояний между локациями, для выполнения задачи по вывозу 595 единиц ЖБИ из Нижнекамска нам потребуется 83 рабочих дня.

При сроке исполнения 2 календарных месяца (то есть 44 рабочих дня в среднем), нам необходимо как минимум 2 грузовика с КМУ для того чтобы уложится в срок

  • В некоторые дни у нас цикл (погрузился - везде выгрузился- вернулся) может занимать до 3-х дней, а значит при среднесуточных ожиданиях грузовиков с КМУ не менее 30 тысяч рублей в сутки, стоимость доставки таковых ЖБИ составит 30*3/20=4500 рублей за штуку.

А значит, и бюджет всей перевозки необходимо считать не столько в "рублей за километр", а даже "рублей в сутки с учётом затраченного времени".

Послесловие

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

До встречи!

Хэштеги:

#логистика #исскуственный_интеллект #chatgpt