Вариант 2
Логистика бетона (жидкого цемента) — одна из самых сложных сфер, так как продукт «живет» ограниченное время (схватывается), и цена ошибки высока.
Ниже представлено подробное описание бизнес-процесса, формализация модели и алгоритмика решения.
1. Формализация ситуации: Игровое поле
Мы рассматриваем систему как Динамическое планирование ресурсов с жесткими временными окнами.
Главная цель: Выполнить все заказы клиентов в течение 12-часовой смены, минимизируя простой машин и опоздания, с возможностью мгновенной реакции на хаос (форс-мажор).
Ключевые концепции
-
Смена (Shift): Фиксированный отрезок времени
$T=[0, 720]$(минут), например с 07:00 до 19:00. -
Заказ (Order): Глобальная потребность клиента (например, 20 кубов к 10:00).
-
Поставка (Delivery/Task): Атомарная единица работы. Это одна "ходка" машины.
-
Слот (Slot): Зарезервированный прямоугольник на диаграмме Ганта конкретной машины.
-
Пул (Pool): Множество доступных ресурсов (машин).
2. Атомизация процесса: Жизненный цикл Поставки
Чтобы алгоритм работал, процесс перевозки нужно разбить на этапы, из которых складывается длина слота.
Пусть t — это временные отрезки:
-
Подача (tplant): Время пути от текущего положения машины до бетонного завода (точки погрузки).
-
Погрузка (tload): Технологическое время заливки (например, 15 мин).
-
Доставка (ttravel): Путь до клиента.
-
Разгрузка (tunload): Самый критичный этап. Время, когда клиент принимает бетон. Это «якорь» слота.
-
Форс-мажорный буфер (tbuffer): Добавочное время (например, 10% от пути) на пробки/заминки.
-
Возврат/Очистка (treturn): Путь обратно или на мойку.
Формула слота:
Важно: Машина считается свободной для следующего слота только после завершения treturn (или прибытия в точку следующей погрузки).
3. Акторы, Атрибуты и События
Здесь мы используем событийную модель (Event-Driven). Все взаимодействия идут через интерфейсы.
Актор 1: Грузовик (Truck)
Физический исполнитель.
-
Атрибуты:
-
ID: Уникальный номер. -
Capacity: Объем бочки (например, 5, 7, 10 кубов). -
State: Состояние (Idle, Busy, Maintenance, OutOfService). -
Location: Текущие координаты. -
Schedule: Список назначенных Слотов (List<Slot>).
-
-
Методы:
-
AssignSlot(Slot s): Принять задачу. -
ReportStatus(Status s): Сообщить о поломке/завершении.
-
Актор 2: Заказ (Order)
Родительская сущность.
-
Атрибуты:
-
TotalVolume: Общий объем (например, 15 кубов). -
Location: Точка разгрузки. -
Deadline: Желаемое время (или интервал) начала разгрузки первой машины. -
Priority: Вес клиента (VIP, Стандарт).
-
-
Алгоритм разбиения:
-
Делит
TotalVolumeна части по 5-10 кубов (в зависимости от доступных стандартных объемов машин), создавая список сущностей Поставка (Delivery).
-
Актор 3: Диспетчер (Scheduler / Algorithm)
Мозг системы.
-
Задачи:
-
Мониторинг Пула свободных машин.
-
Расчет скоринга (рейтинга) машин для конкретной поставки.
-
Реакция на события (пересчет расписания).
-
Список Событий (Events)
-
OnOrderReceived: Поступил новый заказ -> Запуск разбиения на поставки. -
OnDeliveryScheduled: Поставка превратилась в Слот на машине. -
OnTruckStatusChanged:-
Broken: Машина сломалась.
-
Delayed: Машина опаздывает на $X$ минут.
-
-
OnShiftEnded: Закрытие смены.
4. Алгоритмика: Распределение и Каскадный перерасчет
Это "сердце" вашей системы. Она должна быть простой и визуализируемой на Ганте.
Алгоритм А: Первичное распределение (Greedy Best-Fit)
Когда появляется потребность в перевозке (Поставка), алгоритм ищет исполнителя:
-
Фильтрация: Из всего парка выбрать машины, у которых
State != OutOfServiceиCapacity >= DeliveryVolume. -
Поиск окон: Для каждой машины проверить ее расписание на Ганте. Есть ли свободное время между существующими слотами (или после последнего), достаточное для Tslot?
-
Скоринг (Взвешивание): Каждому кандидату присваивается балл. Чем меньше балл, тем лучше.
-
Назначение: Машина с лучшим скором получает Слот. Слот «бронирует» время на диаграмме Ганта.
Алгоритм Б: Каскадный перерасчет (Ripple Effect)
Самое интересное происходит при форс-мажоре.
Сценарий 1: Задержка (Delay)
Машина M_1 застряла на разгрузке на 30 минут.
-
Сдвиг: Текущий слот S_1 расширяется вправо на 30 мин.
-
Коллизия: Проверяем следующий слот S_2 этой же машины. Если S_1.End > S_2.Start, то S_2 тоже сдвигается вправо.
-
Валидация: Если сдвиг S_2 приводит к тому, что машина не успевает к клиенту (слишком поздно) или выходит за границы смены:
-
Слот S_2 аннулируется у машины M_1.
-
Поставка из S_2 возвращается в статус "New" с пометкой "High Priority".
-
Запускается Алгоритм А для поиска новой машины для этой поставки.
-
Сценарий 2: Поломка (Breakdown)
Машина M_1 сломалась в 12:00.
-
Блокировка: Статус машины ->
OutOfService. -
Очистка: Все будущие слоты (S_2, S_3...) начиная с 12:00 удаляются у этой машины.
-
Ре-пулинг: Все удаленные поставки выбрасываются в общий пул задач с наивысшим приоритетом.
-
Перераспределение: Система экстренно ищет свободные машины (или окна у занятых) для "сиротских" поставок.
5. Визуализация: Диаграмма Ганта
Представьте таблицу, где:
-
Ось Y: Список машин (Грузовик 1, Грузовик 2...).
-
Ось X: Время (07:00 ... 19:00).
-
Блоки: Цветные прямоугольники (Слоты).
-
Зеленый: Едет/Грузится.
-
Красный: Стоит на разгрузке (основная цель).
-
Серый: Порожний пробег/Возврат.
-
Правило Ганта: Блоки на одной строке (одной машине) не могут пересекаться. Между ними должен быть минимальный технический зазор.
6. Девиации и Подводные камни (Мировая практика)
При разработке учтите эти реальные проблемы логистики бетона:
-
Проблема "Непрерывной заливки":
-
Суть: Часто заказы требуют, чтобы машины приходили одна за другой без перерыва (интервал 10-15 мин), иначе образуется "холодный шов" в фундаменте.
-
Решение: Группа поставок одного заказа должна линковаться. Если одна машина опаздывает, следующая должна (по возможности) ускориться или алгоритм должен переставить другую машину вперед. Это "Связанные слоты".
-
-
Остатки бетона (Return Concrete):
-
Суть: Клиент заказал 5 кубов, вылил 4. В машине остался 1 куб. Бетон нельзя везти другому клиенту (марка, время жизни).
-
Решение: Машина помечается флагом "Dirty", она обязана ехать на утилизацию/промывку перед следующим заказом. Это увеличивает время $t_{return}$.
-
-
Ограничения на въезд:
-
В центре города могут ездить только машины до 5 тонн. Алгоритм фильтрации должен учитывать атрибут
ZoneRestrictionу машины и заказа.
-
Проект архитектуры на C#, который переводит описанную выше логику в строгие типы и контракты.
Логические блоки: Примитивы времени (база для Ганта), Сущности (Данные) и Акторы (Поведение).
1. Базовые примитивы (Основа Диаграммы Ганта)
Самое важное — правильно работать со временем и пересечениями. Создадим структуру TimeWindow, чтобы не писать каждый раз проверки Start < End.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConcreteLogistics.Core
{
// Структура для любого временного отрезка на диаграмме Ганта
public struct TimeWindow
{
public DateTime Start { get; }
public DateTime End { get; }
public TimeSpan Duration => End - Start;
public TimeWindow(DateTime start, TimeSpan duration)
{
Start = start;
End = start.Add(duration);
}
public TimeWindow(DateTime start, DateTime end)
{
if (end < start) throw new ArgumentException("End cannot be before Start");
Start = start;
End = end;
}
// Ключевой метод для алгоритма распределения
// Проверяет, накладывается ли этот слот на другой
public bool IntersectsWith(TimeWindow other)
{
return Start < other.End && other.Start < End;
}
// Проверка, помещается ли слот внутри смены
public bool IsWithin(TimeWindow outerWindow)
{
return Start >= outerWindow.Start && End <= outerWindow.End;
}
}
}
2. Данные и Состояния (Enums & Models)
Определим словари состояний и метрики, из которых состоит слот.
namespace ConcreteLogistics.Core
{
public enum TruckStatus
{
Idle, // Свободен, стоит в парке
Active, // Выполняет слот (едет, грузится, разгружается)
Maintenance, // Плановое ТО
OutOfService // Поломка/ДТП (Критично для перерасчета)
}
public enum DeliveryPriority
{
Standard = 0,
High = 1, // Важный клиент
Critical = 2 // "Горящий" заказ после сбоя (Cascade Recovery)
}
// Детализация времени внутри слота (из чего складывается длина кирпичика на Ганте)
public class SlotMetrics
{
public TimeSpan TravelToPlant { get; set; } // Подача
public TimeSpan Loading { get; set; } // Погрузка
public TimeSpan TravelToSite { get; set; } // Путь к клиенту
public TimeSpan Unloading { get; set; } // Разгрузка (Ядро слота)
public TimeSpan ReturnBase { get; set; } // Возврат
public TimeSpan Buffer { get; set; } // Форс-мажорный запас
public TimeSpan TotalDuration =>
TravelToPlant + Loading + TravelToSite + Unloading + ReturnBase + Buffer;
}
}
3. Основные Интерфейсы (Акторы)
Поставка (IDelivery) и Слот (ISlot)
Поставка — это потребность. Слот — это поставка, "прибитая" к конкретному времени и машине.
namespace ConcreteLogistics.Interfaces
{
// Атомарная задача на перевозку (часть большого Заказа)
public interface IDelivery
{
Guid Id { get; }
Guid OrderId { get; } // Ссылка на родительский заказ
double Volume { get; } // Например, 5.0 кубов
GeoLocation TargetLocation { get; }
TimeWindow DesiredDeliveryWindow { get; } // Когда клиент хочет получить бетон
DeliveryPriority Priority { get; set; }
}
// Запланированная работа на диаграмме Ганта
public interface ISlot
{
Guid Id { get; }
ITruck AssignedTruck { get; }
IDelivery Delivery { get; }
// Временные рамки
TimeWindow Window { get; } // Start = начало подачи, End = конец возврата
// Точное время, когда бетон начнет литься (для клиента)
DateTime PouringStartTime { get; }
// Метрики для анализа эффективности
SlotMetrics Metrics { get; }
// Флаг для перекрытий
bool Overlaps(ISlot other);
}
}
Грузовик (ITruck)
Машина знает о своих характеристиках и расписании.
namespace ConcreteLogistics.Interfaces
{
public interface ITruck
{
string RegistrationNumber { get; }
double MaxCapacity { get; } // Вместимость бочки
TruckStatus CurrentStatus { get; }
GeoLocation CurrentLocation { get; }
// Расписание машины (список слотов)
// Должно быть отсортировано по времени
IReadOnlyList<ISlot> Schedule { get; }
// Проверка: Может ли машина взять эту поставку в это время?
// Внутри проверяется Capacity и пересечения TimeWindow с существующими слотами
bool CanAccept(IDelivery delivery, TimeWindow proposedWindow);
// Назначение слота
void AssignSlot(ISlot slot);
// Событие: Машина сломалась или освободилась
void UpdateStatus(TruckStatus newStatus);
// Метод для очистки расписания при поломке (возвращает отмененные слоты)
IEnumerable<ISlot> ClearFutureSlots(DateTime fromTime);
}
}
4. Мозг системы: Планировщик и События
Здесь реализована событийная модель, о которой мы говорили.
namespace ConcreteLogistics.Interfaces
{
// Аргументы событий для шины данных
public class RescheduleRequiredEventArgs : EventArgs
{
public string Reason { get; set; } // Например, "Truck 5 Breakdown"
public List<IDelivery> OrphanedDeliveries { get; set; } // Поставки, потерявшие машину
}
public interface IFleetScheduler
{
// 1. Входные данные: Пул машин и Смена
void InitializeShift(IEnumerable<ITruck> fleet, TimeWindow shiftDuration);
// 2. Основной метод: Обработка нового заказа
// Разбивает заказ на Deliveries и пытается найти слоты
bool ProcessOrder(IOrder order);
// 3. Метод ручного или автоматического поиска машины для конкретной поставки
// Возвращает true, если слот успешно создан
bool TryFindSlot(IDelivery delivery, out ISlot scheduledSlot);
// --- Событийная модель ---
// Подписка на изменение статуса машин (для каскадного пересчета)
void OnTruckStatusChanged(ITruck truck, TruckStatus oldStatus, TruckStatus newStatus);
// Событие, которое выбрасывает Планировщик, если случился форс-мажор,
// и требуются действия оператора или полный пересчет
event EventHandler<RescheduleRequiredEventArgs> RescheduleRequired;
}
// Родительский заказ
public interface IOrder
{
Guid Id { get; }
double TotalVolume { get; }
// Метод разбиения большого объема на части (например, 15 -> 5 + 5 + 5)
IEnumerable<IDelivery> SplitIntoDeliveries(double standardTruckVolume);
}
}
5. Пример логики: Проверка коллизий (Implementation Sketch)
Вот как выглядит реализация метода CanAccept внутри класса Truck, чтобы вы увидели, как работает математика "без перекрытий".
public class ConcreteTruck : ITruck
{
// ... инициализация свойств ...
private List<ISlot> _schedule = new List<ISlot>();
public bool CanAccept(IDelivery delivery, TimeWindow proposedWindow)
{
// 1. Базовая проверка физики
if (delivery.Volume > this.MaxCapacity) return false;
if (this.CurrentStatus == TruckStatus.OutOfService) return false;
// 2. Проверка на пересечение с уже существующими слотами
// Используем метод IntersectsWith из нашей структуры TimeWindow
bool hasOverlap = _schedule.Any(existingSlot =>
existingSlot.Window.IntersectsWith(proposedWindow));
if (hasOverlap) return false;
// 3. (Опционально) Проверка географической связности
// Успеет ли машина доехать от конца предыдущего слота до начала нового?
// Это обычно закладывается в TravelToPlant, но можно проверить явно.
return true;
}
}
Рекомендации по реализации:
-
GeoLocation: Для простоты пока можно использовать заглушку (X, Y) и считать расстояние по прямой (Manhattan or Euclidean), но в реальности лучше сразу заложить интерфейс
IRoutingService, который будет возвращать реальное время в пути с учетом пробок. -
Immutability: Старайтесь делать
TimeWindowиSlotMetricsнеизменяемыми (immutable). Это спасет от ошибок при многопоточном пересчете расписания. -
Потокобезопасность: Если планировщик работает в веб-сервисе, доступ к списку
_scheduleвнутри грузовика должен быть защищен (lock), так как диспетчер может читать его, пока водитель обновляет статус.
Ниже представлена реализация «мозга» системы. В нем объединена математика скоринга, поиск временных окон (Time Windows) и логика каскадного перерасчета в чистый C# код.
Для работы этого кода подразумевается использование интерфейсов (ITruck, ISlot и т.д.).
1. Вспомогательные сервисы (Mock-up)
В реальности здесь будет вызов Google Maps API или OSRM. Для алгоритма нам нужна имитация расчета времени пути.
public static class RouteCalculator
{
// Средняя скорость грузовика (км/ч) -> км/мин
private const double AvgSpeedKmPerMin = 40.0 / 60.0;
public static TimeSpan CalculateTravelTime(GeoLocation from, GeoLocation to)
{
// Упрощенная евклидова метрика для примера
double distance = Math.Sqrt(Math.Pow(from.X - to.X, 2) + Math.Pow(from.Y - to.Y, 2));
double minutes = distance / AvgSpeedKmPerMin;
// Добавляем 10% на светофоры
return TimeSpan.FromMinutes(minutes * 1.1);
}
}
2. Алгоритм I: Разбиение заказа (Order Splitting)
Превращает «Хочу 18 кубов бетона» в набор конкретных задач для автопарка.
public class OrderSplitter
{
public IEnumerable<IDelivery> SplitOrder(IOrder order, double standardTruckCapacity = 7.0)
{
var deliveries = new List<IDelivery>();
double remainingVolume = order.TotalVolume;
int partNumber = 1;
// Пока есть что возить
while (remainingVolume > 0)
{
// Берем либо полную машину, либо остаток
double volumeToTake = Math.Min(remainingVolume, standardTruckCapacity);
// Создаем сущность Поставки (реализация IDelivery)
var delivery = new DeliveryTask
{
Id = Guid.NewGuid(),
OrderId = order.Id,
Volume = volumeToTake,
TargetLocation = order.Location,
Priority = order.Priority,
// Для бетона важно: каждая следующая машина должна прийти через 15-20 мин
// чтобы не было "холодного шва". Сдвигаем желаемое время.
DesiredDeliveryWindow = new TimeWindow(
order.Deadline.AddMinutes((partNumber - 1) * 20),
TimeSpan.FromMinutes(30) // Окно допуска
)
};
deliveries.Add(delivery);
remainingVolume -= volumeToTake;
partNumber++;
}
return deliveries;
}
}
3. Алгоритм II: Поиск окна и Скоринг (Allocation Logic)
Это самая сложная часть. Нужно найти не просто свободную машину, а «лучшую» машину.
public class AllocationAlgorithm
{
// Весовые коэффициенты для скоринга (можно вынести в конфиг)
private const double WeightDistance = 1.0; // Важность близости
private const double WeightWaste = 50.0; // Штраф за полупустую бочку (очень важно)
private const double WeightDelay = 2.0; // Штраф за отклонение от желаемого времени
/// <summary>
/// Ищет лучшую машину и точный слот для конкретной поставки
/// </summary>
public ISlot FindBestSlot(IEnumerable<ITruck> fleet, IDelivery delivery, TimeWindow shiftLimit)
{
ISlot bestSlot = null;
double bestScore = double.MaxValue;
// 1. Фильтрация пула (исключаем сломанные и маленькие машины)
var candidates = fleet
.Where(t => t.CurrentStatus != TruckStatus.OutOfService)
.Where(t => t.MaxCapacity >= delivery.Volume);
foreach (var truck in candidates)
{
// 2. Пытаемся найти свободное время в расписании грузовика
var candidateSlot = FindFirstAvailableGap(truck, delivery, shiftLimit);
if (candidateSlot != null)
{
// 3. Расчет рейтинга (Score)
double score = CalculateScore(truck, delivery, candidateSlot);
// Если этот вариант лучше найденных ранее — запоминаем
if (score < bestScore)
{
bestScore = score;
bestSlot = candidateSlot;
}
}
}
return bestSlot; // Вернет null, если машин нет
}
private ISlot FindFirstAvailableGap(ITruck truck, IDelivery delivery, TimeWindow shiftLimit)
{
// Сортируем существующие слоты по времени
var orderedSlots = truck.Schedule.OrderBy(s => s.Window.Start).ToList();
// Точка старта поиска (либо начало смены, либо "сейчас", если смена уже идет)
DateTime searchStart = DateTime.Now > shiftLimit.Start ? DateTime.Now : shiftLimit.Start;
// Начальная локация (база или текущее местоположение)
GeoLocation lastLocation = truck.CurrentLocation;
// Проход по "дыркам" между слотами
// Добавляем фиктивный слот в конец, чтобы проверить время после последней задачи
for (int i = 0; i <= orderedSlots.Count; i++)
{
DateTime gapStart = (i == 0) ? searchStart : orderedSlots[i - 1].Window.End;
DateTime gapEnd = (i == orderedSlots.Count) ? shiftLimit.End : orderedSlots[i].Window.Start;
// Если дырка схлопнулась (нет времени), пропускаем
if (gapEnd <= gapStart) continue;
// --- Симуляция физики слота ---
// 1. Время доехать до завода (от последней точки)
// (В реальности точка погрузки фиксирована - завод)
var plantLocation = new GeoLocation(0, 0); // Допустим, завод в (0,0)
TimeSpan timeToPlant = RouteCalculator.CalculateTravelTime(lastLocation, plantLocation);
// 2. Время начала погрузки (самое раннее)
DateTime readyToLoad = gapStart.Add(timeToPlant);
// 3. Полный расчет метрик слота
var metrics = new SlotMetrics
{
TravelToPlant = timeToPlant,
Loading = TimeSpan.FromMinutes(15), // Константа погрузки
TravelToSite = RouteCalculator.CalculateTravelTime(plantLocation, delivery.TargetLocation),
Unloading = TimeSpan.FromMinutes(30), // Разгрузка
ReturnBase = RouteCalculator.CalculateTravelTime(delivery.TargetLocation, plantLocation), // Возврат на базу/завод
Buffer = TimeSpan.FromMinutes(15) // Буфер
};
// 4. Проверка: влезает ли вся эта активность в дырку?
DateTime slotEnd = readyToLoad.Add(metrics.TotalDuration);
if (slotEnd <= gapEnd)
{
// УРА! Слот найден. Создаем объект (но пока не назначаем)
return new ConcreteSlot(truck, delivery, readyToLoad, metrics);
}
// Обновляем локацию для следующей итерации (конец текущего слота)
if (i < orderedSlots.Count)
lastLocation = orderedSlots[i].Delivery.TargetLocation; // Или база, зависит от логики
}
return null; // Нет места
}
private double CalculateScore(ITruck truck, IDelivery delivery, ISlot slot)
{
// 1. Порожний пробег (плохо)
double emptyRun = slot.Metrics.TravelToPlant.TotalMinutes + slot.Metrics.ReturnBase.TotalMinutes;
// 2. Потеря вместимости (везти 5 кубов в 10-кубовой машине — плохо)
double wastedCapacity = truck.MaxCapacity - delivery.Volume;
// 3. Отклонение от желаемого времени клиента
double timeDeviation = Math.Abs((slot.PouringStartTime - delivery.DesiredDeliveryWindow.Start).TotalMinutes);
// Формула целевой функции (минимизация)
return (emptyRun * WeightDistance) +
(wastedCapacity * WeightWaste) +
(timeDeviation * WeightDelay);
}
}
4. Алгоритм III: Каскадный перерасчет (The Scheduler)
Этот класс связывает всё воедино и реагирует на форс-мажор.
public class FleetScheduler : IFleetScheduler
{
private readonly List<ITruck> _fleet;
private readonly AllocationAlgorithm _algo;
private readonly TimeWindow _shift;
// Очередь "сиротских" заказов (тех, чьи машины сломались)
private Queue<IDelivery> _emergencyQueue = new Queue<IDelivery>();
public FleetScheduler(List<ITruck> fleet, TimeWindow shift)
{
_fleet = fleet;
_shift = shift;
_algo = new AllocationAlgorithm();
}
// --- Реакция на поломку (Cascade Logic) ---
public void ReportTruckBreakdown(string truckId)
{
var truck = _fleet.FirstOrDefault(t => t.RegistrationNumber == truckId);
if (truck == null) return;
Console.WriteLine($"[ALERT] Truck {truckId} broken down. Initiating cascade recalculation.");
// 1. Меняем статус
truck.UpdateStatus(TruckStatus.OutOfService);
// 2. Очищаем всё будущее расписание этой машины с текущего момента
// Метод ClearFutureSlots должен вернуть список отмененных слотов
var cancelledSlots = truck.ClearFutureSlots(DateTime.Now);
// 3. Извлекаем поставки и повышаем им приоритет
foreach (var slot in cancelledSlots)
{
var delivery = slot.Delivery;
delivery.Priority = DeliveryPriority.Critical; // Помечаем как "Горящий"
_emergencyQueue.Enqueue(delivery);
}
// 4. Запускаем экстренное перераспределение
ProcessEmergencyQueue();
}
private void ProcessEmergencyQueue()
{
while (_emergencyQueue.Count > 0)
{
var delivery = _emergencyQueue.Dequeue();
// Пытаемся найти новую машину
var newSlot = _algo.FindBestSlot(_fleet, delivery, _shift);
if (newSlot != null)
{
// Нашли! Назначаем
newSlot.AssignedTruck.AssignSlot(newSlot);
Console.WriteLine($"[RECOVERY] Delivery {delivery.Id} reassigned to Truck {newSlot.AssignedTruck.RegistrationNumber}");
}
else
{
// КРИТИЧЕСКАЯ СИТУАЦИЯ: Нет свободных машин
// Здесь можно реализовать логику "вытеснения" (Preemption)
// Например, отменить менее важный заказ у другой машины, чтобы вставить этот.
Console.WriteLine($"[CRITICAL] Could not reassign delivery {delivery.Id}. Human intervention required.");
// Можно бросить событие для UI диспетчера
}
}
}
}
Разбор ключевых моментов
-
Логика
FindFirstAvailableGap: Это прямая реализация вашего требования "без перекрытий". Мы идем хронологически:Конец предыдущего->Начало следующего. ЕслиНачало следующего - Конец предыдущего > Длительность требуемого слота, значит, мы можем туда встать. -
Форс-мажор (
ReportTruckBreakdown): Обратите внимание, что мы не просто удаляем слоты, а сохраняемIDelivery(суть заказа) и кладем их в_emergencyQueue. Это гарантирует, что работа не потеряется. -
Приоритет: При аварии мы ставим
DeliveryPriority.Critical. В алгоритме скоринга можно добавить проверку: если приоритет Критический, можно игнорировать "waste capacity" (везти 1 куб в 10-кубовой машине), лишь бы спасти заказ.
Модуль Времени (Time Ticker)
Этот модуль реализует принцип "Шаг симуляции", продвигая время вперед и проверяя, какие запланированные события должны были произойти в этот интервал. Для того, чтобы система могла не только планировать, но и динамически реагировать.
1. Структура Событий Симулятора
Определим типы событий, которые будут генерироваться при наступлении ключевых моментов слота.
using System;
using System.Collections.Generic;
namespace ConcreteLogistics.Simulation
{
// Общий базовый класс для событий симуляции
public abstract class SimulationEventArgs : EventArgs
{
public DateTime EventTime { get; }
public ISlot Slot { get; }
public string Message { get; }
protected SimulationEventArgs(ISlot slot, DateTime time, string message)
{
Slot = slot;
EventTime = time;
Message = message;
}
}
// Событие 1: Машина прибыла на завод и готова к погрузке
public class TruckReadyToLoadEvent : SimulationEventArgs
{
public TruckReadyToLoadEvent(ISlot slot, DateTime time)
: base(slot, time, $"Грузовик {slot.AssignedTruck.RegistrationNumber} готов к погрузке.") { }
}
// Событие 2: Началась разгрузка на объекте клиента (Pouring Start)
public class DeliveryStartedEvent : SimulationEventArgs
{
public DeliveryStartedEvent(ISlot slot, DateTime time)
: base(slot, time, $"Началась разгрузка по заказу {slot.Delivery.OrderId}.") { }
}
// Событие 3: Поставка завершена (конец разгрузки + возврат)
public class DeliveryCompletedEvent : SimulationEventArgs
{
public DeliveryCompletedEvent(ISlot slot, DateTime time)
: base(slot, time, $"Поставка {slot.Delivery.Id} полностью завершена.") { }
}
}
2. Модуль Времени (Simulation Engine)
Класс SimulationEngine будет отвечать за продвижение времени и проверку расписания.
namespace ConcreteLogistics.Simulation
{
public interface ISimulationEngine
{
DateTime CurrentTime { get; }
// События, которые нужно отслеживать
event EventHandler<TruckReadyToLoadEvent> OnReadyToLoad;
event EventHandler<DeliveryCompletedEvent> OnDeliveryCompleted;
// Главный метод: продвижение времени
void AdvanceTime(TimeSpan step);
}
public class SimulationEngine : ISimulationEngine
{
private DateTime _currentTime;
private readonly IEnumerable<ITruck> _fleet;
private readonly TimeWindow _shiftBoundary;
public DateTime CurrentTime => _currentTime;
public event EventHandler<TruckReadyToLoadEvent> OnReadyToLoad;
public event EventHandler<DeliveryCompletedEvent> OnDeliveryCompleted;
public SimulationEngine(IEnumerable<ITruck> fleet, TimeWindow shiftBoundary)
{
_fleet = fleet;
_shiftBoundary = shiftBoundary;
_currentTime = shiftBoundary.Start;
}
public void AdvanceTime(TimeSpan step)
{
DateTime nextTime = _currentTime.Add(step);
// Если выходим за пределы смены, останавливаемся
if (nextTime > _shiftBoundary.End)
{
nextTime = _shiftBoundary.End;
}
// Проверяем расписание всех машин
foreach (var truck in _fleet)
{
foreach (var slot in truck.Schedule)
{
// Предполагаем, что ISlot имеет доступ к ключевым моментам:
// Slot.ReadyToLoadTime = Время начала погрузки
// Slot.DeliveryEndTime = Время окончания разгрузки + возврат
// 1. Проверяем событие "Готов к погрузке"
if (_currentTime < slot.ReadyToLoadTime && slot.ReadyToLoadTime <= nextTime)
{
OnReadyToLoad?.Invoke(this, new TruckReadyToLoadEvent(slot, slot.ReadyToLoadTime));
}
// 2. Проверяем событие "Поставка завершена"
if (_currentTime < slot.Window.End && slot.Window.End <= nextTime)
{
// В конце слота (Window.End) машина возвращается в пул
OnDeliveryCompleted?.Invoke(this, new DeliveryCompletedEvent(slot, slot.Window.End));
}
}
}
// Двигаем время вперед
_currentTime = nextTime;
}
}
}
3. Интеграция с Планировщиком (IFleetScheduler)
Для того чтобы события влияли на систему, Планировщик (Scheduler) должен на них подписаться.
3.1. Реакция на завершение поставки
При завершении поставки машина освобождается, и планировщик должен вернуть ее в пул свободных ресурсов (TruckStatus.Idle).
// Часть класса FleetScheduler (пример подписки)
public class FleetScheduler : IFleetScheduler
{
// ... поля и методы ...
public void ConnectToEngine(ISimulationEngine engine)
{
// Подписка на ключевые события
engine.OnDeliveryCompleted += HandleDeliveryCompleted;
}
private void HandleDeliveryCompleted(object sender, DeliveryCompletedEvent e)
{
var truck = e.Slot.AssignedTruck;
// 1. Обновляем статус машины
truck.UpdateStatus(TruckStatus.Idle);
Console.WriteLine($"[STATUS] Грузовик {truck.RegistrationNumber} вернулся в пул.");
// 2. Здесь можно запустить проверку очереди отложенных (High Priority) заказов,
// используя свободную машину.
// ProcessEmergencyQueue();
}
// ... (и подписка на другие события, например, для логирования)
}
3.2. Добавление "случайных" форс-мажоров
Для полноценного симулятора в SimulationEngine можно добавить небольшой шанс (например, 1% на каждом шаге) на возникновение форс-мажора, который будет запускать каскадный перерасчет.
public void AdvanceTime(TimeSpan step)
{
// ... код проверки расписания ...
// --- Имитация Форс-мажора (1% шанс на шаг) ---
if (new Random().Next(1, 100) == 1)
{
var activeTrucks = _fleet.Where(t => t.CurrentStatus == TruckStatus.Active).ToList();
if (activeTrucks.Any())
{
var brokenTruck = activeTrucks[new Random().Next(activeTrucks.Count)];
// Вместо прямого вызова, нужно сгенерировать событие,
// которое подхватит планировщик и вызовет ReportTruckBreakdown
// (e.g., OnTruckDisrupted?.Invoke(brokenTruck.RegistrationNumber));
Console.WriteLine($"[SIM] Имитация поломки у {brokenTruck.RegistrationNumber}!");
}
}
_currentTime = nextTime;
}
Следующий шаг:
Логика распределения, скоринга и симулятор времени. Финальный шаг — объединить все это в рабочий пример, где мы:
-
Создаем 5 машин.
-
Создаем 3 заказа.
-
Запускаем
FleetScheduler.ProcessOrder(). -
Запускаем цикл
SimulationEngine.AdvanceTime(), наблюдая за реакцией системы.
Единый рабочий сценарий симуляции на C#
Для простоты и наглядности созданы простые заглушки (Mock) для интерфейсов ITruck, ISlot и IDelivery, чтобы продемонстрировать, как основные алгоритмы (AllocationAlgorithm, FleetScheduler, SimulationEngine) взаимодействуют в течение смены.
1. Подготовка: Имитация Сущностей (Mocks)
Создадим минимальные реализации интерфейсов для демонстрации.
// --- Имитация Геолокации ---
public class GeoLocation { public double X { get; set; } public double Y { get; set; } }
// --- Имитация Смены ---
public class ShiftInfo
{
public static readonly TimeWindow ShiftWindow =
new TimeWindow(DateTime.Today.AddHours(7), TimeSpan.FromHours(12));
}
// --- Имитация ITruck, IDelivery, ISlot (Упрощено для примера) ---
public class MockTruck : ITruck
{
// ... [реализация ITruck (Status, Schedule, AssignSlot и т.д.)] ...
// Включая метод ClearFutureSlots, который возвращает отмененные ISlot.
// ...
}
public class MockDelivery : IDelivery
{
// ... [реализация IDelivery] ...
}
public class MockSlot : ISlot
{
// ... [реализация ISlot, включая PouringStartTime, ReadyToLoadTime, Window] ...
// [Эти значения рассчитываются AllocationAlgorithm]
// ...
}
2. Сценарий Симуляции (Simulation Scenario)
Мы прогоним симуляцию в три фазы: Планирование, Исполнение и Реакция на Форс-мажор.
using System;
using System.Linq;
using System.Collections.Generic;
using ConcreteLogistics.Interfaces;
using ConcreteLogistics.Simulation;
using ConcreteLogistics.Core; // Для TimeWindow, TruckStatus и т.д.
public class SimulationScenario
{
public static void RunSimulation()
{
Console.WriteLine("=== СТАРТ СИМУЛЯЦИИ ЛОГИСТИКИ БЕТОНА ===");
// 1. Инициализация Пула Машин
var fleet = new List<ITruck>
{
new MockTruck { RegistrationNumber = "Т01", MaxCapacity = 7.0, CurrentLocation = new GeoLocation { X = 1, Y = 1 } },
new MockTruck { RegistrationNumber = "Т02", MaxCapacity = 10.0, CurrentLocation = new GeoLocation { X = 2, Y = 2 } },
new MockTruck { RegistrationNumber = "Т03", MaxCapacity = 7.0, CurrentLocation = new GeoLocation { X = 50, Y = 50 } } // Далеко
};
// 2. Инициализация Диспетчера и Симулятора
var scheduler = new FleetScheduler(fleet, ShiftInfo.ShiftWindow);
var engine = new SimulationEngine(fleet, ShiftInfo.ShiftWindow);
scheduler.ConnectToEngine(engine); // Подписка на события завершения поставок
// 3. Планирование: Заказы
var orderA = new MockOrder
{
TotalVolume = 14.0,
Deadline = DateTime.Today.AddHours(10), // К 10 утра
Location = new GeoLocation { X = 15, Y = 15 },
Priority = DeliveryPriority.High
};
var orderB = new MockOrder
{
TotalVolume = 5.0,
Deadline = DateTime.Today.AddHours(12), // К 12 дня
Location = new GeoLocation { X = 4, Y = 4 },
Priority = DeliveryPriority.Standard
};
Console.WriteLine("\n--- ФАЗА I: ПЛАНИРОВАНИЕ ЗАКАЗОВ ---");
// Диспетчер обрабатывает заказы (разбивает и назначает)
scheduler.ProcessOrder(orderA); // 14м³ => 2 поставки по 7м³
scheduler.ProcessOrder(orderB); // 5м³ => 1 поставка
Console.WriteLine($"\nВсего назначено слотов: {fleet.Sum(t => t.Schedule.Count)}");
Console.WriteLine($"Расписание T01: {fleet.First(t => t.RegistrationNumber == "Т01").Schedule.Count} слотов.");
// 4. Исполнение: Запуск Тикера
Console.WriteLine("\n--- ФАЗА II: ИСПОЛНЕНИЕ И МОНИТОРИНГ ---");
// Запускаем симулятор с шагом в 30 минут, пока смена не закончится
while (engine.CurrentTime < ShiftInfo.ShiftWindow.End)
{
TimeSpan step = TimeSpan.FromMinutes(30);
// Если осталось меньше 30 минут, берем остаток
if (engine.CurrentTime.Add(step) > ShiftInfo.ShiftWindow.End)
{
step = ShiftInfo.ShiftWindow.End - engine.CurrentTime;
}
Console.WriteLine($"\n[ТИКЕР] Время: {engine.CurrentTime.ToShortTimeString()} - {engine.CurrentTime.Add(step).ToShortTimeString()}");
// Продвигаем время и генерируем события
engine.AdvanceTime(step);
// Имитация форс-мажора (ТОЛЬКО для T01 в 10:30)
if (engine.CurrentTime.Hour == 10 && engine.CurrentTime.Minute == 30)
{
// 5. Каскадный перерасчет
Console.WriteLine("\n--- ФАЗА III: КАСКАДНЫЙ ПЕРЕРАСЧЕТ (ФОРС-МАЖОР) ---");
scheduler.ReportTruckBreakdown("Т01"); // T01 ломается в середине смены
}
// Если симуляция достигла конца, выходим
if (engine.CurrentTime >= ShiftInfo.ShiftWindow.End) break;
}
Console.WriteLine("\n=== СИМУЛЯЦИЯ ЗАВЕРШЕНА ===");
}
}
3. Анализ и Реакция Системы
Вот как система реагирует на сбой в Фазе III, используя ваши алгоритмы:
-
Начальное Планирование:
-
Заказ A (14м³): Разбивается на два слота по 7м³.
-
Слот A1 и Слот A2, скорее всего, назначаются на Т01 и Т02, так как они ближе всего к заводу (низкий
WeightDistanceв Скоринге).
-
-
Заказ B (5м³): Назначается на машину с лучшим Скорингом (возможно, Т03, если у Т01/Т02 нет свободного окна, или Т02 благодаря близости и вместимости).
-
-
Форс-мажор (10:30):
-
Событие: Диспетчер вызывает
scheduler.ReportTruckBreakdown("Т01"). -
Реакция
FleetScheduler(Алгоритм Б):-
Т01переводится вTruckStatus.OutOfService. -
Все будущие слоты на
Т01(например, Слот A2) удаляются. -
Удаленные
IDelivery(Слот A2) добавляются в_emergencyQueueсPriority = Critical. -
Запускается
ProcessEmergencyQueue().
-
-
Перераспределение:
-
Алгоритм
FindBestSlotищет машину для потерянного слота A2. -
Т01 игнорируется. Т03 (дальняя) может получить Слот A2, если Т02 занят, несмотря на большое время порожнего пробега, потому что
Priorityважнее.
-
-
-
Завершение Поставки:
-
Когда наступает время
Window.Endдля Т02,SimulationEngineгенерируетOnDeliveryCompleted. -
FleetSchedulerловит это событие и вызываетT02.UpdateStatus(TruckStatus.Idle), возвращая машину в пул для выполнения экстренных или новых заказов.
-
Этот сценарий наглядно демонстрирует, как событийная модель и каскадный перерасчет позволяют системе самостоятельно адаптироваться к неожиданным поломкам.


