
В этом посте мы посмотрим, как Flexport использует математику и науку о данных для решения проблемы доставки и доставляет грузы вовремя при их наименьшей возможной стоимости.
Рассмотрим умозрительный сценарий: у экспедитора десять отправлений и один рейс назначения любой отгрузки. Единственное решение, которое нужно принять - назначить ли каждую отгрузку этому единственному полету. Если мы не назначаем определенный груз полету, предположим, что возможно переместить его другим способом.
У каждой отгрузки есть объем и стоимость, а рейс ограничен в объеме. Вы можете подумать об этом как об упрощенной проблеме рюкзака. Итак, есть 1024-1=1023 возможных решений (мы не отправили бы самолет полностью пустым).
Мы могли бы создать электронную таблицу, чтобы перечислить все решение и выбрать самое выгодное из них. Но что, если у вас те же десять отправлений, но два рейса? Это 59 049 решений всего за 10 отправлений.
У крупного экспедитора более десяти отправлений и, конечно, более двух рейсов на выбор, что приводит к триллионам на триллионы возможных решений. Среди них только миллионы осуществимы, а значит, традиционный метод электронных таблиц может найти по крайней мере одно осуществимое решение. Но нам не нужны только осуществимые решения. Нам нужны лучшие, оптимальные решения. Как найти их среди бесчисленных возможностей? Один из ответов - использовать целочисленное программирование.
Целочисленное программирование - это подраздел дискретной оптимизации, область исследования операций, связанных с минимизацией некоторой целевой функции, подверженной ограничениям. Мы хотим минимизировать общие затраты при условии своевременной доставки грузов в нужные пункты, укладываясь в ULD (Unit Load Device - средство пакетирования грузов). Мы стремимся к оптимальному решению, но на практике иногда не можем его достичь. В этом случае мы довольны хорошим или близким решением. Здесь ограничимся простой моделью, в которой оптимальное решение достижимо.
Первый шаг при решении подобной проблемы - обращение к литературе. Научное сообщество уже много лет занимается вопросами экспедирования грузов. Мы нашли несколько работ, которые очень напоминают нашу проблему. Мы взяли многие из следующих понятий и обозначений из этих работ и благодарим авторов за их вклад в эту область.
Начнем с определения целевой функции. Чтобы минимизировать затраты, нам нужно понять концепцию стандартного веса. Говоря коротко, стандартный вес - это минимальный вес, с которым экспедитор обязуется работать, независимо от того, какой вес предлагается фактически. У нас есть общий вес, стандартный вес и коэффициенты для перегрузок и, наоборот, недовеса. Стандартный вес, умноженный на коэффициент недовеса - это недооценка, поэтому мы можем игнорировать недовес и сосредоточиться на коэффициенте перегрузки, умноженном на саму перегрузку.
Целевая функция - минимизация общей стоимости, определяемой как общий вес всех грузов, присвоенных ULD, умноженный на коэффициент перегрузки. Например, если у ULD1 100 килограммов перегрузки, а ставка за перегрузку для ULD1 – 4 доллара за килограмм, то общая стоимость ULD 1 составит 400 долларов. Итак, нам нужна некоторая нотация для избыточного веса и для его стоимости.
Пусть
Значение для
Общий вес ULD, конечно, зависит от того, какие грузы отнесены к ULD, и их веса. Поэтому нам нужно выражение для его вычисления, включающее упомянутые выше детали.
Это просто сумма весов грузов, назначенных ULD. Как указать, что партия товара была назначена конкретному ULD? Для этого нам нужен не параметр, а переменная решения. Переменная решения - это то, что решатель может контролировать при минимизации целевой функции.
Пусть параметр
Например,
Пусть
Давайте выпишем этот расчет общего веса в общем виде. Определим общий вес ULD
Дополнительный вес
Теперь, когда у нас есть общий вес, мы можем применить нашу формулу для дополнительной нагрузки:
Например, если
Вот, чего мы действительно хотим:
Это то же самое, что просто установить 0 для переменной, что так же просто, как создать ограничение
Итак, мы реализовали функцию max() в математическом программировании: a = max (b, c), то есть a >= b && a >= c. Давайте посмотрим на наши определения.
Целевая функция:
Каждый груз должен лететь
На этом этапе мы могли бы написать это на Python и отправить его решателю. Если бы мы это сделали, то обнаружили бы, что решатель назначил нулевые поставки любому рейсу, и мы можем быстро понять почему: лучший способ минимизировать целевую функцию - не накапливать никаких затрат. Это приводит к следующему ограничению: каждая партия должна быть назначена какому-то ULD. Мы распространим это на каждый груз, который должен быть назначен одному и только одному ULD, хотя в действительности мы можем разделить груз на несколько ULD и даже на несколько рейсов.
Это означает, что
При наличии этого ограничения решатель фактически начнет назначать отгрузки рейсам, но он просто распределит отгрузки между всеми доступными полетами, пока не будут выполнены весовые коэффициенты. Затем решатель поместил бы каждую оставшуюся партию груза в один ULD с наименьшими дополнительными расходами. Без учета объема или веса этого ULD. Итак, следующие добавляемые ограничения - объем и вес.
Ограничения объема и веса
Давайте определимся
Ветеран отрасли сразу же поймет, что это не соответствует действительности. Почему? Потому что эти ограничения рассматривают груз так, как будто его можно вылить, как воду, в любой объем. В реальности грузы жесткие или имеют другие ограничения, например, по укладке. 10 кубических метров груза не могут быть упакованы в произвольный объем, равный именно 10 кубическим метрам. Чтобы справиться с этими случаями, нужно решить задачу об упаковке в контейнеры. Мы проверяем, поместятся ли определенные объемы внутри других, но это выходит за рамки данной статьи.
Теперь решатель назначает отгрузки ULD, соблюдая максимальный вес и объем при минимизации общей стоимости. Но есть еще одна проблема: мы ничего не сказали о назначении отправлений и о соблюдении сроков доставки. На самом деле существует четыре метки времени, которые необходимо учитывать при назначении отгрузки:
Время, когда отгрузка фактически готова к консолидации с другими отгрузками в ULD и загрузке на рейс. Давайте используем
Время, к которому груз должен быть выгружен в пункте назначения, деконсолидирован и доступен для получения, как правило, из грузового терминала. Давайте используем $Q_i^+$ для представления крайнего срока поставки отгрузки
Время, к которому весь груз должен быть доставлен в грузовой терминал для погрузки на рейс. Давайте используем
Время прибытия рейса: это время, к которому груз на рейсе становится доступным для получения на складе назначения. Давайте используем
Источник и назначение
Введем еще один набор
Теперь мы можем использовать
Сроки поставки
При работе с временными метками в оптимизации удобно представлять нужные моменты как время Unix. Плюсы подхода:
- Хранение дат как чисел.
- Не нужно обрабатывать часовые пояса.
- Решатель использует прямые сравнения «больше-меньше».
Рейс должен прибыть до срока доставки груза:
Обратите внимание, что так же, как и для вышеприведенное ограничение по отправлению и назначению, мы указали, что это ограничение действует только для ULD в наборе
Полная модель
При таких ограничениях решатель назначает отправления ULD наименее затратным способом, при этом каждый груз будет доставляться из правильного места отправления в правильное место назначения. Конечно же, груз доставляется вовремя и без перегрузки отправлений по объему или весу.
Параметры
Переменные решения и целевая функция:
Целевая функция
Ограничения
Дополнительный вес jE это max(0, yj-UjP):
Каждая поставка должна быть назначена ровно на 1 ULD:
ULD
ULD
Дальнейшие шаги
В этой статье описывается математическое программирование, лежащее в основе назначения груза полету. Но просто записать математику недостаточно. Следующий шаг - реализация программы, вероятно, в AML, как Pyomo, или с использованием собственного API решателя, например, Python API Gurobi. После этого разработчик напишет код для передачи параметров всех доступных отправлений и рейсов. Затем экземпляр модели отправляется решателю. Решатель установит значения переменных решения оптимальным образом. Затем разработчик должен что-то сделать со значениями переменных принятия решений.
