Вариант 4
Формализация бизнес-процесса планирования и каскадного перераспределения поставок жидкого цемента.
1. Вербальное описание бизнес-процесса и его формализация
Цель системы: Оптимальное и надежное распределение парка грузовиков-миксеров (автобетоносмесителей) для выполнения заказов на доставку жидкого цемента (бетона) в течение рабочей смены (12 часов) с возможностью оперативного перепланирования при сбоях.
Ключевой принцип: Система работает с поставками (слотами) — атомарными единицами работы одной машины для части заказа. Заказ дробится на поставки, которые назначаются на временную диаграмму (аналог диаграммы Ганта) для каждой машины. Непересекаемость слотов у одной машины — главное ограничение.
Основной поток:
1. Планирование на начало смены: Имеется пул заказов, каждый из которых характеризуется общим объемом, точкой разгрузки (клиентом), желательным временным окном (например, "до 15:00").
2. Дробление заказов: Для каждого заказа рассчитывается количество необходимых поставок, исходя из стандартного объема машины (например, 5 м³) или доступных машин разной вместимости. Образуется очередь запланированных поставок.
3. Назначение машин на слоты: Для каждой поставки в очереди система ищет подходящую машину из свободного пула. "Свободность" означает, что у машины есть незанятый временной интервал, достаточный для выполнения этой поставки (с учетом времени на дорогу, разгрузку, форс-мажорный буфер). Критерии выбора машины для слота:
* Приоритет 1 (обязательный): Наличие свободного временного окна.
* Приоритет 2: Вместимость цистерны (минимизация остатка, соответствие объему поставки).
* Приоритет 3: Близость текущего местоположения машины (или точки окончания предыдущей поставки) к точке погрузки завода.
* Приоритет 4: Приоритет клиента (может влиять на выбор среди равных по другим параметрам).
4. Формирование расписания: Назначенные поставки превращаются в слоты в расписании конкретных машин с четко определенным временем: *выезд с завода -> дорога -> разгрузка у клиента -> возврат на завод/следующая точка погрузки*.
5. Исполнение и мониторинг: В течение смены система отслеживает статус машин и поставок (``Выполняется``, ``Завершена``, ``Задержана``).
6. Реакция на события (Каскадный перерасчет): При наступлении события (например, поломка машины ``МашинаСломана``) система выполняет:
* Снятие слотов: Все будущие слоты этой машины помечаются как ``Свободны`` и возвращаются в пул невыполненных поставок.
* Перераспределение: Запускается алгоритм перераспределения для пула невыполненных поставок, но с учетом текущего состояния всех машин: их реального местоположения, занятости в оставшееся время смены.
* Оптимизация: Алгоритм пытается "вписать" освободившиеся поставки в существующие расписания других машин, возможно, сдвигая их более поздние слоты (каскадный сдвиг), или назначает на вновь освободившиеся машины.
* Уведомления: Все заинтересованные стороны (диспетчер, клиенты при серьезных задержках) получают оповещения об изменениях.
Девиации по мировой практике:
* Досрочное завершение поставки: Машина возвращается в пул раньше, что может позволить "подтянуть" следующие поставки.
* Динамическое изменение заказа: Клиент может увеличить/уменьшить объем, что ведет к пересчету числа поставок и перепланированию.
* Изменение точки разгрузки: Возможно на этапе планирования или даже исполнения (редко).
* "Жадное" планирование: Некоторые системы оставляют резервные машины в простое для подстраховки, что снижает эффективность, но повышает устойчивость.
2. Список акторов, сущностей, событий и алгоритмов
АКТОРЫ (Роли в системе)
1. Диспетчер (Пользователь системы): Инициирует планирование, вносит изменения, получает уведомления.
2. Система планирования (Ядро алгоритма): Главный вычислительный модуль, распределяет слоты и реагирует на события.
3. Водитель/Машина (Источник данных): Через телематику предоставляет данные о местоположении, статусе выполнения, техническом состоянии.
СУЩНОСТИ (Бизнес-объекты)
1. Заказ (Order):
* Атрибуты: ID, Клиент, Адрес разгрузки, Общий объем (м³), Желаемое время окончания, Приоритет.
2. Поставка (Delivery / Slot):
* Атрибуты: ID, Ссылка на Заказ, Объем поставки (м³), Статус (`Запланирована`, `Назначена`, `В пути`, `Разгрузка`, `Выполнена`, `Отменена`), Расчетное время: `t_погрузки`, `t_начала_разгрузки`, `t_окончания_разгрузки`.
* Ключевая идея: Это задача в календаре машины.
3. Машина (Truck / Vehicle):
* Атрибуты: ID, Текущий статус (`Свободна`, `Назначена`, `В пути`, `Разгрузка`, `Неисправна`), Вместимость (м³), Текущее местоположение (GPS), Текущий остаток цемента, Расписание (упорядоченный список слотов `List<Slot>`).
4. Пул машин (Fleet): Коллекция всех машин. Имеет методы для выборки подмножества по критериям.
5. Расписание (Schedule): Совокупность расписаний всех машин. Визуализируется как диаграмма Ганта.
6. Событие (Event):
* Атрибуты: Тип, Время возникновения, Связанная Сущность (Машина/Поставка), Данные (например, новая задержка в минутах).
СОБЫТИЯ (Event Types)
1. `ПоставкаЗавершена` — Машина освободилась.
2. `МашинаСломана` — Требуется снять все будущие слоты и перераспределить.
3. `ЗадержкаНаПоставке` — Текущая операция затянулась, сдвигает все последующие слоты этой машины и может повлиять на другие.
4. `НоваяСвободнаяМашина` — Машина введена в эксплуатацию (починена, возвращена из аренды).
5. `НовыйСрочныйЗаказ` — Требуется вписать в существующее расписание.
6. `ИзменениеОбъемаЗаказа` — Требуется добавить или отменить поставки.
АЛГОРИТМЫ И ИХ ПАРАМЕТРЫ
Алгоритм 1: Первоначальное распределение (Initial Assignment)
* Вход: Список заказов, Пул машин (все свободны на начало смены).
* Выход: Начальное расписание.
* Логика:
1. Преобразовать все заказы в очередь поставок, отсортированную по `Желаемому времени окончания` и `Приоритету клиента`.
2. Для каждой поставки в очереди:
* Вызвать Функцию поиска машины.
* Если машина найдена — закрепить слот за ней, обновить ее расписание и статус.
* Если не найдена — отложить поставку в список проблемных (требует ручного вмешательства или расширения пула).
* Параметры: Весовые коэффициенты для критериев выбора машины (близость, вместимость, приоритет).
Алгоритм 2: Функция поиска машины для слота (FindTruckForSlot)
* Вход: Поставка (с объемом, точкой разгрузки, расчетной длительностью), Пул машин.
* Выход: Лучшая машина и конкретное временное окно для слота.
* Логика:
1. Отфильтровать машины по вместимости (`вместимость >= объем_поставки`).
2. Для каждой подходящей машины проанализировать ее текущее расписание. Найти все возможные окна между существующими слотами, куда новая поставка может поместиться (с учетом времени на дорогу от предыдущей точки, погрузку, дорогу до клиента, разгрузку + буфер).
3. Если окна есть, выбрать из них самое раннее.
4. Среди машин, нашедших окна, выбрать лучшую по взвешенной оценке: `Оценка = w1 * (время_начала_окна) + w2 * (коэф_заполнения_цистерны) + w3 * (расстояние_до_завода)`.
* Параметры: Длительность буфера на форс-мажор (например, 15-20% от времени поставки), веса `w1, w2, w3`.
Алгоритм 3: Каскадный перерасчет (Cascade Rescheduling)
* Вход: Событие (например, `МашинаСломана`), Текущее расписание.
* Выход: Обновленное расписание.
* Логика:
1. Локализация последствий: Определить множество поставок, которые требуют переноса (все будущие слоты сломанной машины + потенциально слоты других машин, которые могли бы быть сдвинуты для улучшения плана).
2. Освобождение слотов: Удалить эти поставки из расписаний, поместив их в список `ПоставкиКПерепланированию`.
3. Перепланирование: Запустить упрощенный алгоритм 1, но:
* На вход подается список `ПоставкиКПерепланированию`.
* Пул машин рассматривается с их текущим состоянием и частично заполненным расписанием.
* Алгоритм пытается вставить поставки в существующие расписания, сдвигая более поздние задачи (каскад). Это может быть реализовано через повторные вызовы `FindTruckForSlot`.
4. Валидация: Проверить, что все критичные по времени заказы (например, которые должны быть выполнены до конца смены) все еще выполнимы.
* Параметры: Глубина перепланирования (только непосредственно затронутые машины или все), флаг разрешения/запрета увеличения длительности смены для конкретных машин.
3. Подводные камни и рекомендации
1. Сложность каскадного перерасчета: Полный пересчет всей диаграммы Ганта при каждом событии может быть тяжелым. Необходимо ограничивать область перепланирования ("локальный ремонт" расписания).
2. Нерешаемая ситуация: При множественных сбоях или нехватке машин система может не найти решение. Требуется механизм эскалации к диспетчеру с понятным отчетом о проблеме (например, "Заказ #456 не может быть выполнен в срок").
3. Качество данных: Точность расчетов времени движения зависит от актуальных данных о пробках. Интеграция с картографическими сервисами (Google Maps, Яндекс.Карты) через API необходима.
4. Психологический фактор: Частое перепланирование и сдвиги времени для водителей могут демотивировать. Возможное решение — вводить "заморозку" ближайшего слота (например, в течение часа) от текущего момента, чтобы не менять задачи водителю, который уже готовится к рейсу.
5. Простота vs. Оптимальность: Алгоритмы, описанные выше, являются жадными эвристиками. Они быстрые и дают хорошее решение, но не гарантируют глобальной оптимальности (например, по общему пробегу парка). Для задач такого масштама (десятки машин, сотни поставок) этого обычно достаточно.
6. Визуализация: Диаграмма Ганта должна обновляться в реальном времени. Цветовая индикация статуса (выполнено, в пути, задержано) и машины (исправна, неисправна) критически важна для диспетчера.
Заключение
Предложенная модель четко разделяет сущности, процессы и события. Она основана на принципах дискретного событийного моделирования и жадного алгоритма планирования с приоритетной очередью. Система реагирует на события через механизм перепланирования, что обеспечивает устойчивость к сбоям. Для реализации стоит начать с прототипа, реализующего основные алгоритмы 1-3, и простой визуализации в виде диаграммы Ганта, что полностью соответствует поставленным требованиям простоты и наглядности.
Полный прототип системы распределения машин для перевозки цемента
1. Основные классы сущностей
// DeliveryPlanningSystem.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace CementDeliverySystem
{
#region Enums (Перечисления)
public enum TruckStatus
{
Free, // Свободна
Assigned, // Назначена на поставку
Loading, // Загрузка на заводе
InTransit, // В пути к клиенту
Unloading, // Разгрузка у клиента
Returning, // Возвращение на базу
Broken, // Сломана/в ремонте
OutOfOrder // Неисправна (не может быть использована)
}
public enum DeliveryStatus
{
Planned, // Запланирована
Assigned, // Назначена на машину
Loading, // Загрузка
InTransit, // В пути
Unloading, // Разгрузка
Completed, // Завершена
Canceled, // Отменена
Delayed // Задержана
}
public enum EventType
{
DeliveryCompleted,
TruckBroken,
DeliveryDelayed,
NewTruckAvailable,
NewUrgentOrder,
OrderVolumeChanged,
ScheduleUpdated
}
#endregion
#region Models (Модели данных)
// Точка на карте (координаты)
public class Location
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public Location(double lat, double lon)
{
Latitude = lat;
Longitude = lon;
}
// Упрощенное расстояние между точками (в км)
public double DistanceTo(Location other)
{
// Для прототипа используем упрощенную формулу
var latDiff = Math.Abs(Latitude - other.Latitude);
var lonDiff = Math.Abs(Longitude - other.Longitude);
return Math.Sqrt(latDiff * latDiff + lonDiff * lonDiff) * 111; // 1 градус ≈ 111 км
}
}
// Заказ клиента
public class Order
{
public string Id { get; set; }
public string ClientName { get; set; }
public Location DeliveryAddress { get; set; }
public double TotalVolume { get; set; } // Общий объем в м³
public DateTime DesiredCompletionTime { get; set; }
public int Priority { get; set; } // 1-10, где 10 - высший приоритет
public DateTime CreatedAt { get; set; }
public Order(string id, string client, Location address, double volume,
DateTime completionTime, int priority = 5)
{
Id = id;
ClientName = client;
DeliveryAddress = address;
TotalVolume = volume;
DesiredCompletionTime = completionTime;
Priority = priority;
CreatedAt = DateTime.Now;
}
}
// Поставка (слот в расписании)
public class Delivery
{
public string Id { get; set; }
public Order Order { get; set; }
public double Volume { get; set; } // Объем этой поставки
public DeliveryStatus Status { get; set; }
// Расчетные времена
public DateTime PlannedLoadingTime { get; set; }
public DateTime PlannedStartUnloadingTime { get; set; }
public DateTime PlannedEndUnloadingTime { get; set; }
// Фактические времена (заполняются при выполнении)
public DateTime? ActualLoadingTime { get; set; }
public DateTime? ActualStartUnloadingTime { get; set; }
public DateTime? ActualEndUnloadingTime { get; set; }
// Связанные объекты
public Truck AssignedTruck { get; set; }
public TimeSpan BufferTime { get; set; } // Буфер на форс-мажор
// Метод для расчета длительности поставки
public TimeSpan GetTotalDuration()
{
var loadingTime = TimeSpan.FromMinutes(15); // 15 минут на загрузку
var travelTimeToClient = TimeSpan.FromMinutes(30); // Упрощенно, должно быть из расчета расстояния
var unloadingTime = TimeSpan.FromMinutes(Volume * 2); // 2 минуты на м³
var returnTime = TimeSpan.FromMinutes(30); // Возвращение на базу
return loadingTime + travelTimeToClient + unloadingTime + returnTime + BufferTime;
}
// Проверка, перекрывается ли с другим слотом
public bool OverlapsWith(Delivery other)
{
var thisStart = PlannedLoadingTime;
var thisEnd = PlannedEndUnloadingTime;
var otherStart = other.PlannedLoadingTime;
var otherEnd = other.PlannedEndUnloadingTime;
return thisStart < otherEnd && otherStart < thisEnd;
}
}
// Машина (грузовик-миксер)
public class Truck
{
public string Id { get; set; }
public string LicensePlate { get; set; }
public double Capacity { get; set; } // Вместимость в м³
public TruckStatus Status { get; set; }
public Location CurrentLocation { get; set; }
public double CurrentCementVolume { get; set; } // Текущий остаток цемента
// Расписание машины (упорядоченный список поставок)
public List<Delivery> Schedule { get; private set; }
// Техническое состояние
public bool IsOperational { get; set; } = true;
public DateTime? EstimatedRepairTime { get; set; }
public Truck(string id, string plate, double capacity, Location initialLocation)
{
Id = id;
LicensePlate = plate;
Capacity = capacity;
CurrentLocation = initialLocation;
Schedule = new List<Delivery>();
Status = TruckStatus.Free;
}
// Добавление поставки в расписание с проверкой перекрытий
public bool AddToSchedule(Delivery delivery)
{
// Проверяем, что машина исправна
if (!IsOperational || Status == TruckStatus.Broken || Status == TruckStatus.OutOfOrder)
return false;
// Проверяем перекрытие со всеми существующими поставками
foreach (var existingDelivery in Schedule)
{
if (delivery.OverlapsWith(existingDelivery))
{
return false;
}
}
Schedule.Add(delivery);
delivery.AssignedTruck = this;
// Сортируем расписание по времени
Schedule = Schedule.OrderBy(d => d.PlannedLoadingTime).ToList();
return true;
}
// Удаление поставки из расписания
public bool RemoveFromSchedule(string deliveryId)
{
var delivery = Schedule.FirstOrDefault(d => d.Id == deliveryId);
if (delivery != null)
{
Schedule.Remove(delivery);
return true;
}
return false;
}
// Получение ближайшего свободного временного окна
public DateTime? GetNextAvailableTime(TimeSpan requiredDuration)
{
if (Schedule.Count == 0)
return DateTime.Now;
// Ищем окна между поставками
for (int i = 0; i < Schedule.Count - 1; i++)
{
var currentEnd = Schedule[i].PlannedEndUnloadingTime;
var nextStart = Schedule[i + 1].PlannedLoadingTime;
var gap = nextStart - currentEnd;
if (gap >= requiredDuration)
{
return currentEnd;
}
}
// Если окна не найдены, возвращаем время после последней поставки
return Schedule.Last().PlannedEndUnloadingTime;
}
// Проверка доступности в заданный интервал
public bool IsAvailable(DateTime start, DateTime end)
{
foreach (var delivery in Schedule)
{
if (start < delivery.PlannedEndUnloadingTime &&
end > delivery.PlannedLoadingTime)
{
return false;
}
}
return true;
}
}
// Событие системы
public class SystemEvent
{
public EventType Type { get; set; }
public DateTime OccurredAt { get; set; }
public string Description { get; set; }
public object RelatedEntity { get; set; } // Машина, поставка и т.д.
public Dictionary<string, object> Data { get; set; }
public SystemEvent(EventType type, string description, object relatedEntity = null)
{
Type = type;
Description = description;
RelatedEntity = relatedEntity;
OccurredAt = DateTime.Now;
Data = new Dictionary<string, object>();
}
}
#endregion
#region Interfaces (Интерфейсы)
// Интерфейс для алгоритма планирования
public interface IPlannerAlgorithm
{
List<Delivery> CreateDeliveriesFromOrder(Order order, double standardVolume);
Truck FindBestTruckForDelivery(Delivery delivery, List<Truck> availableTrucks);
void CascadeReschedule(SystemEvent triggerEvent, List<Truck> allTrucks);
}
// Интерфейс для визуализации
public interface IScheduleVisualizer
{
void DisplaySchedule(List<Truck> trucks);
void DisplayGanttChart(List<Truck> trucks, DateTime shiftStart, DateTime shiftEnd);
}
// Интерфейс для обработчика событий
public interface IEventHandler
{
void HandleEvent(SystemEvent systemEvent);
}
#endregion
#region Core Algorithm Implementation (Реализация алгоритмов)
// Основной алгоритм планирования
public class GreedyPlanner : IPlannerAlgorithm
{
private readonly Location _plantLocation; // Местоположение завода
private readonly double _bufferPercentage = 0.15; // 15% буфер на форс-мажор
private readonly TimeSpan _minBufferTime = TimeSpan.FromMinutes(15);
public GreedyPlanner(Location plantLocation)
{
_plantLocation = plantLocation;
}
// Алгоритм 1: Создание поставок из заказа
public List<Delivery> CreateDeliveriesFromOrder(Order order, double standardVolume)
{
var deliveries = new List<Delivery>();
var remainingVolume = order.TotalVolume;
var deliveryCount = 1;
while (remainingVolume > 0)
{
var deliveryVolume = Math.Min(standardVolume, remainingVolume);
remainingVolume -= deliveryVolume;
var delivery = new Delivery
{
Id = $"{order.Id}-D{deliveryCount++}",
Order = order,
Volume = deliveryVolume,
Status = DeliveryStatus.Planned,
BufferTime = CalculateBufferTime(deliveryVolume)
};
deliveries.Add(delivery);
}
return deliveries;
}
// Алгоритм 2: Поиск лучшей машины для поставки
public Truck FindBestTruckForDelivery(Delivery delivery, List<Truck> availableTrucks)
{
// Фильтруем доступные машины
var suitableTrucks = availableTrucks
.Where(t => t.IsOperational &&
t.Capacity >= delivery.Volume &&
t.Status != TruckStatus.Broken &&
t.Status != TruckStatus.OutOfOrder)
.ToList();
if (!suitableTrucks.Any())
return null;
// Рассчитываем длительность поставки
var deliveryDuration = delivery.GetTotalDuration();
// Оцениваем каждую машину
var scoredTrucks = new List<(Truck truck, double score)>();
foreach (var truck in suitableTrucks)
{
// 1. Находим ближайшее доступное время
var availableTime = truck.GetNextAvailableTime(deliveryDuration);
if (!availableTime.HasValue)
continue;
// 2. Рассчитываем оценку
double score = 0;
// Время до начала окна (чем раньше, тем лучше)
var timeToStart = (availableTime.Value - DateTime.Now).TotalMinutes;
score += 1000 / (timeToStart + 1); // +1 чтобы избежать деления на 0
// Коэффициент заполнения (чем ближе к 100%, тем лучше)
var fillRatio = delivery.Volume / truck.Capacity;
score += fillRatio * 500;
// Расстояние до завода (чем ближе, тем лучше)
var distanceToPlant = truck.CurrentLocation.DistanceTo(_plantLocation);
score += 300 / (distanceToPlant + 1);
// Приоритет клиента влияет на вес времени
score *= (1 + delivery.Order.Priority / 10.0);
scoredTrucks.Add((truck, score));
}
// Выбираем машину с максимальной оценкой
return scoredTrucks
.OrderByDescending(t => t.score)
.FirstOrDefault()
.truck;
}
// Алгоритм 3: Каскадный перерасчет
public void CascadeReschedule(SystemEvent triggerEvent, List<Truck> allTrucks)
{
Console.WriteLine($"=== Каскадный перерасчет по событию: {triggerEvent.Description} ===");
var affectedDeliveries = new List<Delivery>();
switch (triggerEvent.Type)
{
case EventType.TruckBroken:
var brokenTruck = triggerEvent.RelatedEntity as Truck;
if (brokenTruck != null)
{
// Снимаем все будущие поставки сломанной машины
var futureDeliveries = brokenTruck.Schedule
.Where(d => d.PlannedLoadingTime > DateTime.Now)
.ToList();
affectedDeliveries.AddRange(futureDeliveries);
// Освобождаем слоты
foreach (var delivery in futureDeliveries)
{
brokenTruck.RemoveFromSchedule(delivery.Id);
delivery.Status = DeliveryStatus.Planned; // Возвращаем в пул планирования
delivery.AssignedTruck = null;
}
brokenTruck.Status = TruckStatus.Broken;
brokenTruck.IsOperational = false;
}
break;
case EventType.DeliveryDelayed:
var delayedDelivery = triggerEvent.RelatedEntity as Delivery;
if (delayedDelivery != null && delayedDelivery.AssignedTruck != null)
{
// Находим все последующие поставки этой машины
var truck = delayedDelivery.AssignedTruck;
var index = truck.Schedule.IndexOf(delayedDelivery);
if (index >= 0)
{
var subsequentDeliveries = truck.Schedule.Skip(index + 1).ToList();
affectedDeliveries.AddRange(subsequentDeliveries);
// Сдвигаем все последующие поставки
var delayMinutes = triggerEvent.Data.ContainsKey("delayMinutes")
? (int)triggerEvent.Data["delayMinutes"]
: 30;
var delay = TimeSpan.FromMinutes(delayMinutes);
foreach (var delivery in subsequentDeliveries)
{
delivery.PlannedLoadingTime += delay;
delivery.PlannedStartUnloadingTime += delay;
delivery.PlannedEndUnloadingTime += delay;
}
}
}
break;
case EventType.NewTruckAvailable:
// Новые машины автоматически будут использованы при следующем планировании
break;
}
// Перепланирование освободившихся поставок
if (affectedDeliveries.Any())
{
Console.WriteLine($"Перепланируем {affectedDeliveries.Count} поставок...");
// Получаем список всех машин, кроме сломанных
var operationalTrucks = allTrucks
.Where(t => t.IsOperational &&
t.Status != TruckStatus.Broken &&
t.Status != TruckStatus.OutOfOrder)
.ToList();
// Пытаемся перераспределить каждую поставку
foreach (var delivery in affectedDeliveries)
{
var bestTruck = FindBestTruckForDelivery(delivery, operationalTrucks);
if (bestTruck != null)
{
// Назначаем поставку на ближайшее доступное время
var availableTime = bestTruck.GetNextAvailableTime(delivery.GetTotalDuration());
if (availableTime.HasValue)
{
delivery.PlannedLoadingTime = availableTime.Value;
delivery.PlannedStartUnloadingTime = availableTime.Value + TimeSpan.FromMinutes(45); // Загрузка + дорога
delivery.PlannedEndUnloadingTime = delivery.PlannedStartUnloadingTime +
TimeSpan.FromMinutes(delivery.Volume * 2); // Разгрузка
bestTruck.AddToSchedule(delivery);
delivery.Status = DeliveryStatus.Assigned;
Console.WriteLine($" Поставка {delivery.Id} переназначена на {bestTruck.LicensePlate} " +
$"на {delivery.PlannedLoadingTime:HH:mm}");
}
}
else
{
Console.WriteLine($" Внимание: Поставка {delivery.Id} не может быть переназначена!");
delivery.Status = DeliveryStatus.Delayed;
}
}
}
}
private TimeSpan CalculateBufferTime(double volume)
{
var baseTime = TimeSpan.FromMinutes(volume * 0.5); // 0.5 мин на м³
var buffer = TimeSpan.FromTicks((long)(baseTime.Ticks * _bufferPercentage));
return buffer > _minBufferTime ? buffer : _minBufferTime;
}
}
#endregion
#region Core System (Основная система)
// Диспетчерская система
public class DispatchSystem : IEventHandler
{
private List<Truck> _trucks;
private List<Order> _orders;
private List<Delivery> _allDeliveries;
private List<SystemEvent> _eventLog;
private readonly IPlannerAlgorithm _planner;
private readonly IScheduleVisualizer _visualizer;
private readonly Location _plantLocation;
private const double StandardTruckVolume = 5.0; // Стандартный объем 5 м³
public DispatchSystem(Location plantLocation, IPlannerAlgorithm planner = null, IScheduleVisualizer visualizer = null)
{
_plantLocation = plantLocation;
_planner = planner ?? new GreedyPlanner(plantLocation);
_visualizer = visualizer ?? new ConsoleVisualizer();
_trucks = new List<Truck>();
_orders = new List<Order>();
_allDeliveries = new List<Delivery>();
_eventLog = new List<SystemEvent>();
InitializeSampleData();
}
private void InitializeSampleData()
{
// Инициализация тестовых данных
var rand = new Random();
// Создаем несколько машин
for (int i = 1; i <= 5; i++)
{
var capacity = i % 2 == 0 ? 5.0 : 7.0; // Разная вместимость
var location = new Location(
_plantLocation.Latitude + (rand.NextDouble() * 0.1 - 0.05),
_plantLocation.Longitude + (rand.NextDouble() * 0.1 - 0.05)
);
_trucks.Add(new Truck($"TRUCK-{i}", $"А{100 + i}БВ", capacity, location));
}
// Создаем несколько заказов
var orders = new List<Order>
{
new Order("ORD-001", "СтройМир", new Location(55.75, 37.62), 15.0,
DateTime.Today.AddHours(16), 7),
new Order("ORD-002", "Теплый Дом", new Location(55.76, 37.58), 8.0,
DateTime.Today.AddHours(14), 5),
new Order("ORD-003", "СтройГрад", new Location(55.74, 37.60), 12.0,
DateTime.Today.AddHours(18), 8),
new Order("ORD-004", "Срочный заказ", new Location(55.77, 37.59), 5.0,
DateTime.Today.AddHours(12), 10) // Высокий приоритет
};
_orders.AddRange(orders);
}
// Основной метод распределения на смену
public void PlanShift(DateTime shiftStart, DateTime shiftEnd)
{
Console.WriteLine($"=== ПЛАНИРОВАНИЕ СМЕНЫ: {shiftStart:HH:mm} - {shiftEnd:HH:mm} ===");
// 1. Создаем поставки из всех заказов
var allDeliveries = new List<Delivery>();
foreach (var order in _orders.OrderByDescending(o => o.Priority)
.ThenBy(o => o.DesiredCompletionTime))
{
var deliveries = _planner.CreateDeliveriesFromOrder(order, StandardTruckVolume);
allDeliveries.AddRange(deliveries);
}
_allDeliveries = allDeliveries;
Console.WriteLine($"Создано {allDeliveries.Count} поставок из {_orders.Count} заказов");
// 2. Распределяем поставки по машинам
int assignedCount = 0;
var freeTrucks = _trucks.Where(t => t.IsOperational).ToList();
// Сортируем поставки по приоритету и времени
var sortedDeliveries = allDeliveries
.OrderByDescending(d => d.Order.Priority)
.ThenBy(d => d.Order.DesiredCompletionTime)
.ToList();
foreach (var delivery in sortedDeliveries)
{
var bestTruck = _planner.FindBestTruckForDelivery(delivery, freeTrucks);
if (bestTruck != null)
{
// Назначаем время поставки (для прототипа - последовательно)
var nextTime = bestTruck.GetNextAvailableTime(delivery.GetTotalDuration()) ?? shiftStart;
delivery.PlannedLoadingTime = nextTime;
delivery.PlannedStartUnloadingTime = nextTime + TimeSpan.FromMinutes(45);
delivery.PlannedEndUnloadingTime = delivery.PlannedStartUnloadingTime +
TimeSpan.FromMinutes(delivery.Volume * 2);
delivery.Status = DeliveryStatus.Assigned;
if (bestTruck.AddToSchedule(delivery))
{
assignedCount++;
Console.WriteLine($" {delivery.Id} ({delivery.Volume} м³) -> {bestTruck.LicensePlate} " +
$"в {delivery.PlannedLoadingTime:HH:mm}");
}
}
}
Console.WriteLine($"Распределено {assignedCount} из {allDeliveries.Count} поставок");
// 3. Визуализируем результат
_visualizer.DisplaySchedule(_trucks);
}
// Обработка событий
public void HandleEvent(SystemEvent systemEvent)
{
_eventLog.Add(systemEvent);
Console.WriteLine($"\n[СОБЫТИЕ] {systemEvent.OccurredAt:HH:mm:ss}: {systemEvent.Description}");
switch (systemEvent.Type)
{
case EventType.TruckBroken:
case EventType.DeliveryDelayed:
case EventType.NewTruckAvailable:
_planner.CascadeReschedule(systemEvent, _trucks);
break;
case EventType.DeliveryCompleted:
var delivery = systemEvent.RelatedEntity as Delivery;
if (delivery?.AssignedTruck != null)
{
delivery.AssignedTruck.Status = TruckStatus.Free;
delivery.Status = DeliveryStatus.Completed;
delivery.ActualEndUnloadingTime = DateTime.Now;
// Освобождаем машину для следующих заданий
var truck = delivery.AssignedTruck;
truck.RemoveFromSchedule(delivery.Id);
}
break;
case EventType.NewUrgentOrder:
var newOrder = systemEvent.RelatedEntity as Order;
if (newOrder != null)
{
_orders.Add(newOrder);
Console.WriteLine($"Добавлен срочный заказ {newOrder.Id}");
// Можно запустить перепланирование для срочного заказа
}
break;
}
// Визуализация после обработки события
_visualizer.DisplaySchedule(_trucks.Where(t => t.Schedule.Any()).ToList());
}
// Симуляция поломки машины
public void SimulateTruckBreakdown(string truckId, string reason = "Поломка двигателя")
{
var truck = _trucks.FirstOrDefault(t => t.Id == truckId);
if (truck != null)
{
var evt = new SystemEvent(EventType.TruckBroken,
$"Машина {truck.LicensePlate} вышла из строя: {reason}", truck);
HandleEvent(evt);
}
}
// Симуляция задержки поставки
public void SimulateDeliveryDelay(string deliveryId, int delayMinutes)
{
var delivery = _allDeliveries.FirstOrDefault(d => d.Id == deliveryId);
if (delivery != null)
{
var evt = new SystemEvent(EventType.DeliveryDelayed,
$"Поставка {deliveryId} задерживается на {delayMinutes} минут", delivery);
evt.Data["delayMinutes"] = delayMinutes;
HandleEvent(evt);
}
}
// Добавление новой машины
public void AddNewTruck(Truck truck)
{
_trucks.Add(truck);
var evt = new SystemEvent(EventType.NewTruckAvailable,
$"Добавлена новая машина {truck.LicensePlate}", truck);
HandleEvent(evt);
}
// Получение статистики
public void PrintStatistics()
{
Console.WriteLine("\n=== СТАТИСТИКА СИСТЕМЫ ===");
Console.WriteLine($"Всего машин: {_trucks.Count}");
Console.WriteLine($"Исправных машин: {_trucks.Count(t => t.IsOperational)}");
Console.WriteLine($"Всего заказов: {_orders.Count}");
Console.WriteLine($"Всего поставок: {_allDeliveries.Count}");
Console.WriteLine($"Запланировано: {_allDeliveries.Count(d => d.Status == DeliveryStatus.Assigned)}");
Console.WriteLine($"Выполнено: {_allDeliveries.Count(d => d.Status == DeliveryStatus.Completed)}");
Console.WriteLine($"Задержано: {_allDeliveries.Count(d => d.Status == DeliveryStatus.Delayed)}");
Console.WriteLine($"Всего событий: {_eventLog.Count}");
}
public List<Truck> GetTrucks() => _trucks;
public List<Delivery> GetDeliveries() => _allDeliveries;
}
#endregion
#region Visualizers (Визуализаторы)
// Консольный визуализатор
public class ConsoleVisualizer : IScheduleVisualizer
{
public void DisplaySchedule(List<Truck> trucks)
{
Console.WriteLine("\n=== ТЕКУЩЕЕ РАСПИСАНИЕ ===");
foreach (var truck in trucks.Where(t => t.Schedule.Any()))
{
Console.WriteLine($"\nМашина {truck.LicensePlate} ({truck.Capacity} м³) - {truck.Status}:");
foreach (var delivery in truck.Schedule.OrderBy(d => d.PlannedLoadingTime))
{
var statusSymbol = GetStatusSymbol(delivery.Status);
Console.WriteLine($" {statusSymbol} {delivery.Id}: {delivery.Order.ClientName} " +
$"{delivery.Volume} м³ | {delivery.PlannedLoadingTime:HH:mm} - " +
$"{delivery.PlannedEndUnloadingTime:HH:mm} | {delivery.Status}");
}
}
}
public void DisplayGanttChart(List<Truck> trucks, DateTime shiftStart, DateTime shiftEnd)
{
Console.WriteLine("\n=== ДИАГРАММА ГАНТА (упрощенная) ===");
Console.WriteLine("Время: 08:00 09:00 10:00 11:00 12:00 13:00 14:00 15:00 16:00 17:00 18:00");
Console.WriteLine(" |------|------|------|------|------|------|------|------|------|------|");
foreach (var truck in trucks.Where(t => t.Schedule.Any()))
{
Console.Write($"{truck.LicensePlate.PadRight(8)} ");
// Упрощенный ASCII-график
var hours = new bool[11]; // 8:00-18:00, 11 часов
foreach (var delivery in truck.Schedule)
{
var startHour = delivery.PlannedLoadingTime.Hour - 8;
var endHour = delivery.PlannedEndUnloadingTime.Hour - 7; // +1 час для визуализации
if (startHour >= 0 && endHour <= hours.Length)
{
for (int i = Math.Max(0, startHour); i < Math.Min(endHour, hours.Length); i++)
{
hours[i] = true;
}
}
}
foreach (var occupied in hours)
{
Console.Write(occupied ? "[####] " : "[ ] ");
}
Console.WriteLine();
}
}
private string GetStatusSymbol(DeliveryStatus status)
{
return status switch
{
DeliveryStatus.Assigned => "✓",
DeliveryStatus.InTransit => "→",
DeliveryStatus.Unloading => "↓",
DeliveryStatus.Completed => "✔",
DeliveryStatus.Delayed => "!",
_ => "○"
};
}
}
#endregion
#region Program (Точка входа)
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
// Координаты цементного завода
var plantLocation = new Location(55.7558, 37.6176); // Москва
Console.WriteLine("=== СИСТЕМА ДИСПЕТЧЕРИЗАЦИИ ДОСТАВКИ ЦЕМЕНТА ===\n");
// Создаем систему
var dispatchSystem = new DispatchSystem(plantLocation);
// Планируем смену (8:00 - 18:00)
var shiftStart = DateTime.Today.AddHours(8);
var shiftEnd = DateTime.Today.AddHours(18);
dispatchSystem.PlanShift(shiftStart, shiftEnd);
// Симулируем события в течение смены
Console.WriteLine("\n\n=== СИМУЛЯЦИЯ СОБЫТИЙ В ТЕЧЕНИЕ СМЕНЫ ===\n");
// Событие 1: Поломка машины
Thread.Sleep(1000);
dispatchSystem.SimulateTruckBreakdown("TRUCK-2", "Прокол колеса");
// Событие 2: Задержка поставки
Thread.Sleep(1000);
dispatchSystem.SimulateDeliveryDelay("ORD-001-D1", 45);
// Событие 3: Добавление новой машины
Thread.Sleep(1000);
var newTruck = new Truck("TRUCK-NEW", "Н777НО", 6.0, plantLocation);
dispatchSystem.AddNewTruck(newTruck);
// Событие 4: Срочный новый заказ
Thread.Sleep(1000);
var urgentOrder = new Order("ORD-URGENT", "Аварийный ремонт",
new Location(55.76, 37.63), 4.0,
DateTime.Today.AddHours(15), 10);
var urgentEvent = new SystemEvent(EventType.NewUrgentOrder,
"Поступил срочный заказ на аварийный ремонт", urgentOrder);
dispatchSystem.HandleEvent(urgentEvent);
// Выводим финальную статистику
dispatchSystem.PrintStatistics();
// Визуализируем финальное расписание как диаграмму Ганта
var visualizer = new ConsoleVisualizer();
visualizer.DisplayGanttChart(dispatchSystem.GetTrucks(), shiftStart, shiftEnd);
Console.WriteLine("\n=== СИМУЛЯЦИЯ ЗАВЕРШЕНА ===");
Console.WriteLine("\nНажмите любую клавишу для выхода...");
Console.ReadKey();
}
}
#endregion
}
Ключевые особенности реализации:
1. Полная объектная модель:
-
Все сущности представлены классами с четкой ответственностью
-
Поддержка событийной модели через
SystemEvent -
Интерфейсы для расширения алгоритмов
2. Три основных алгоритма:
-
CreateDeliveriesFromOrder- дробление заказов на поставки -
FindBestTruckForDelivery- поиск оптимальной машины с оценкой по критериям -
CascadeReschedule- каскадный перерасчет при сбоях
3. Гибкая архитектура:
-
Весовые коэффициенты для критериев выбора
-
Настраиваемый буфер на форс-мажор
-
Легко добавлять новые критерии отбора
4. Визуализация:
-
Консольное отображение расписания
-
Упрощенная диаграмма Ганта в ASCII
-
Статусы поставок символами
5. Обработка событий:
-
Поломка машины → перераспределение ее поставок
-
Задержка → каскадный сдвиг последующих поставок
-
Новая машина → перераспределение ожидающих поставок
No Comments