Production Planning / Technical / scheduling-logic.html
Heuristic V4 Solver

Scheduling Logic Technical Specification V4

Đặc tả chi tiết toán học, sơ đồ xử lý, cấu trúc dữ liệu và cài đặt mã nguồn cho giải thuật lập lịch lùi thông minh (Heuristic Backward Scheduling Solver V4).

🧮 Mô Hình Toán Học Bài Toán Lập Lịch V4

Để tối ưu hóa kế hoạch sản xuất và làm nổi bật các điểm nghẽn, solver mô hình hóa các ràng buộc như sau:

1. Định nghĩa các tập hợp & biến số:

  • $WO = \{wo_1, wo_2, ..., wo_k\}$: Tập hợp các Lệnh sản xuất.
  • $WOI = \{woi_1, woi_2, ..., woi_n\}$: Các dòng sản phẩm chi tiết của từng Lệnh.
  • Với mỗi $woi_i$, ta có: $Q_i$ (số lượng), $S_i$ (ngày bắt đầu kế hoạch), $D_i$ (ngày đến hạn - Due Date).
  • $Routing(BOM_i) = \{op_1, op_2, ..., op_m\}$: Cây quy trình công nghệ của chi tiết.
  • $WS_j$ tại ngày $d$ có giới hạn công suất thực tế: $Cap_{j, d}$ (availableHours + otHours).

2. Ràng buộc cứng tiến trình công nghệ (Sequence Constraints):

Công đoạn đứng trước (child) phải hoàn thành trước khi công đoạn đứng sau (parent) khởi chạy một khoảng thời gian trung chuyển (leadTime): $$Date_{parent} \ge Date_{child} + leadTime$$ *Hỗ trợ:* $leadTime = 0$ cho các công đoạn sản xuất song song gối đầu trên cùng một ngày.

3. Ràng buộc giới hạn ngày bắt đầu cứng (Lower Boundary Constraint):

Không có công đoạn nào được lập lịch trước ngày bắt đầu $S_i$ của Lệnh sản xuất. Nếu lượng công việc phân bổ lùi vượt quá $S_i$, toàn bộ phần sản lượng chưa được phân bổ còn lại sẽ được gán trực tiếp vào ngày $S_i$: $$Date_{assignment} \ge S_i$$

📊 Sơ Đồ Tiến Trình Xử Lý Của Solver V4

Luồng xử lý chất tải lùi cô lập theo từng Lệnh sản xuất để đảm bảo hiệu năng và tính đúng đắn:

flowchart TD Start([Bắt đầu Solver V4]) --> Group[Nhóm workOrderItems theo Lệnh sản xuất workOrderId] Group --> ForEachWO{Duyệt từng Lệnh sản xuất WO} ForEachWO -- Hết --> End([Hoàn thành & Tổng hợp kết quả gán]) ForEachWO -- Còn WO --> InitContext[Khởi tạo CapacityContext & AssignmentContext độc lập] InitContext --> Sort[Sắp xếp Items của WO theo ngày bàn giao tăng dần] Sort --> ForEachItem{Duyệt từng dòng sản phẩm Item} ForEachItem -- Còn Item --> AssignRoot[Gọi assign cho Root Node công đoạn cây BOM] AssignRoot --> ForEachItem ForEachItem -- Hết Item --> SaveWO[Lưu assignments của WO vào danh sách chung] SaveWO --> ForEachWO

⚡ Cơ Chế Lập Lịch Cô Lập Cho Từng Lệnh Sản Xuất

Cách giải thuật V4 giải quyết xung đột chiếm dụng công suất giữa các Lệnh sản xuất đồng thời:

Để tránh hiện tượng các Lệnh sản xuất lớn chiếm dụng toàn bộ công suất của các Lệnh sản xuất nhỏ khác một cách ngẫu nhiên gây sai lệch tải dự báo, V4 thực hiện cô lập hoàn toàn môi trường chạy máy:

  • Mỗi Lệnh sản xuất được cấp một bản sao sạch của công suất trạm máy (`CapacityContext`).
  • Hệ thống rải ngược độc lập toàn bộ các mặt hàng trong Lệnh đó để tính toán điểm nghẽn chính xác của riêng Lệnh sản xuất đó.
  • Sau khi hoàn thành, kết quả được gộp lại để hiển thị tổng tải xếp chồng trên sơ đồ Heatmap chung của nhà máy, giúp người làm kế hoạch biết chính xác Lệnh nào gây ra quá tải tại ngày nào.

⚖️ Cơ Chế Gom Cụm Bán Thành Phẩm Đảm Bảo Tròn Số

Cơ chế gom nhóm chi tiết phôi cùng loại để tránh số lượng kế hoạch bị thập phân lẻ:

Hệ thống tự động gom các chi tiết phôi cùng công đoạn (`bomWorkStepId`) và cùng hạn hoàn thành (`deadline`) lại thành một lô sản xuất chung:

// Sử dụng bomWorkStepId và deadline làm khóa hợp nhất
const consolidationKey = `${j.bomWorkStepId}-${j.deadline.format(EDateFormat.DATE)}`;
...
// Cộng trực tiếp số lượng nguyên bản của các chi tiết con
totalConsolidatedQty += j.remainingQty;

Việc cộng trực tiếp số lượng nguyên gốc thay vì quy đổi qua phút rồi chia ngược cho định mức đảm bảo số lượng kế hoạch của các tác vụ chi tiết luôn là số nguyên tuyệt đối, giải quyết triệt để lỗi số lượng lẻ.

💾 Cấu Trúc Database (Prisma Schema DDL)

Cấu trúc dữ liệu lưu trữ kết quả và năng lực trạm máy:

// Bảng đo lường công suất khả dụng của trạm máy theo từng ngày
model TaskCapacity {
  id              String   @id @default(uuid())
  workstationId   String   // Tham chiếu sang Workstation
  date            DateTime // Ngày áp dụng
  availableHours  Float    // Giờ khả dụng tiêu chuẩn (ví dụ: 8.0 giờ)
  allocatedHours  Float    @default(0.0) // Tổng giờ đã được chất tải
  otHours         Float    @default(0.0) // Giờ tăng ca (Overtime) được duyệt thêm
  loadRatio       Float    @default(0.0) // Hệ số chất tải

  @@unique([workstationId, date])
}

// Bảng lưu kết quả phân bổ tác vụ chi tiết xuống MES
model WorkTaskSchedule {
  id              String   @id @default(uuid())
  workOrderItemId String   // Link dòng lệnh sản xuất chi tiết
  operationId     String   // Link công đoạn trong cây BOM
  workstationId   String   // Trạm máy thực hiện
  scheduledDate   DateTime // Ngày chạy máy
  durationHours   Float    // Thời lượng chạy máy thực tế (giờ)
  status          String   @default("SCHEDULED") // SCHEDULED, STARTED, COMPLETED, DELAYED
}

📉 Phân Tích Độ Phức Tạp Thuật Toán V4 (Complexity Analysis)

Đánh giá hiệu năng vận hành thực tế của giải thuật V4:

  • Độ phức tạp thời gian (Time Complexity): $$O(W \cdot (I \log I + I \cdot M \cdot D))$$ Trong đó:
    • $W$: Số lượng Lệnh sản xuất độc lập.
    • $I$: Số lượng Items trong mỗi Lệnh.
    • $M$: Số lượng nút công đoạn trong cây BOM.
    • $D$: Số ngày tối đa từ ngày bắt đầu đến hạn chót (thông thường dưới 30 ngày).
    Vì thuật toán chạy thuần Backward và dồn tải ngay khi chạm biên ngày bắt đầu, không có đệ quy tịnh tiến cha hay quét ngày tương lai không giới hạn, độ phức tạp thời gian thực tế thấp và chạy rất nhanh.
  • Độ phức tạp không gian (Space Complexity): $$O(I \cdot M)$$ Do cô lập trạng thái gán theo từng Lệnh sản xuất, bộ nhớ chỉ lưu trữ cây lùi của một Lệnh tại một thời điểm trước khi tổng hợp kết quả.

🚨 Thiết Kế Xử Lý Quá Tải Đầu Kỳ (Start Date Overload)

Cơ chế thay thế hoàn toàn cho giải pháp Cascade Shifting cũ:

Giải thuật V4 loại bỏ hoàn toàn phương thức dịch chuyển tiến đệ quy `cascadeShiftParent`. Lý do là việc tự động đẩy các công đoạn cha về tương lai sẽ vô tình dồn ứ toàn bộ tải của các công đoạn cuối về sát ngày hạn bàn giao (`deadline`), tạo ra nút thắt cổ chai ảo rất lớn ở cuối kỳ sản xuất.

Quy tắc dồn ứ (Overload Rule):

Khi chất tải lùi một công việc chạm cận ngày bắt đầu (`startDate`): 1. Hệ thống dừng di chuyển lùi về quá khứ. 2. Toàn bộ lượng sản lượng còn lại chưa phân bổ được gán trực tiếp vào ngày `startDate`. 3. Công suất trạm máy ngày `startDate` sẽ bị vượt ngưỡng (overload) tương ứng. 4. Điều này giúp hiển thị chính xác cột màu đỏ/đen cảnh báo quá tải ở đầu kỳ lập lịch trên biểu đồ Heatmap của nhà máy, báo hiệu cho người lập lịch biết cần bổ sung năng lực sản xuất (tăng ca, thêm nhân công) ngay từ ngày đầu tiên.

💻 Cài Đặt Thực Tế Của Solver V4 (NestJS Solver Integration)

Cấu trúc logic lõi của solver `HeuristicBackwardSolverV4` tích hợp trong codebase:

@Injectable()
export class HeuristicBackwardSolverV4 implements ISchedulingSolverGeneral {
  async solve(input: ISchedulingSolverInput): Promise {
    const bomTreeContext = new BomTreeContext(input.bomsWithRelated);
    const allFinalAssignments: any[] = [];
    const allWarnings: any[] = [];

    // 1. Nhóm items theo Lệnh sản xuất để cô lập công suất
    const groupedByWorkOrder = groupBy(input.workOrderItems, 'workOrderId');

    for (const itemsInWorkOrder of Object.values(groupedByWorkOrder)) {
      const workOrderCapacity = new CapacityContext(input.stepCapacities);
      workOrderCapacity.enableExtensionMode(); // Cho phép nới lỏng tạm thời khi dồn tải

      const workOrderAssignment = new AssignmentContext();
      const scheduleContext = new ScheduleContext(
        input.planningStartDate,
        input.planningEndDate,
        workOrderCapacity,
        workOrderAssignment,
        bomTreeContext,
      );

      const assigner = new BackwardAssignerV4(scheduleContext);
      
      // Sắp xếp items theo ngày bàn giao tăng dần (EDD)
      const sortedItems = itemsInWorkOrder.sort((a, b) => a.endDate.compareTo(b.endDate));

      // 2. Chạy lập lịch ngược thuần túy cho từng Item
      for (const workOrderItem of sortedItems) {
        const bomWorkStepTrees = bomTreeContext.getBackwardTree(workOrderItem.bomId);
        if (!bomWorkStepTrees || bomWorkStepTrees.length === 0) {
          throw new BOMNotFoundException();
        }

        bomWorkStepTrees.forEach((tree) => {
          const request: IBackwardAssignmentRequest = {
            workOrderItem,
            quantity: workOrderItem.quantity,
            deadline: workOrderItem.endDate,
            startDate: workOrderItem.startDate,
            parentUuid: undefined,
          };
          assigner.assign(tree, request);
        });
      }

      scheduleContext.assignmentContext.combine();
      allFinalAssignments.push(...scheduleContext.assignmentContext.getAllAssignments());
      allWarnings.push(...scheduleContext.assignmentContext.getWarnings());
    }

    return {
      assignments: allFinalAssignments,
      warnings: allWarnings,
    };
  }
}