package com.aps.service.mp;
import com.aps.common.util.FileHelper;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.*;

import java.io.*;

/**
 * 作者：佟礼
 * 时间：2026-07-23
 * MIP 主生产排程 Demo
 * 多产线 MIP 主生产排程
 * 场景：2条产线(L1, L2)，3种产品(P1, P2, P3)，2天排产
 * 目标：最小化 生产成本 + 库存持有成本 + 换型成本
 * 特性：每条产线效率/成本不同，模型自动选择最优产线分配
 * 按生产顺序计算换型成本：支持任意数量产品的切换
 */
public class MultiLineMpsScheduling {

    //**产线分配**：P1 放在 L1 还是 L2？—— 比较 `成本差` vs `效率差带来的工时影响`
    // **生产批量**：一天生产完还是分两天生产？—— 比较 `换型成本` vs `库存持有成本`
    // **负载均衡**：两条产线谁多干谁少干？—— 在满足交期前提下，优先用单位成本最低的产线
    // **换型权衡**：一天内要不要换型生产两种产品？—— 比较 `换型费` vs `多一天的库存费`
    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {

        // 1. 加载 OR-Tools 本地库（必须）
        Loader.loadNativeLibraries();

        // ========== 2. 定义参数 ==========
        // 产品
        String[] products = {"P1", "P2", "P3"};
        int numProducts = products.length;

        // 每天最多生产的产品数量（用于定义位置变量的维度）
        int maxProductsPerDay = numProducts;

        // 产线
        String[] lines = {"L1", "L2"};
        int numLines = lines.length;

        // 时间段（天）
        int numDays = 2;
        // ========== 3. 创建求解器 ==========
        // 使用 CBC 求解器（开源MIP求解器）
        // MPSolver solver = MPSolver.createSolver("CBC");
        MPSolver solver = new MPSolver("demo",
                MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
        if (solver == null) {
            System.err.println("无法创建求解器，请检查 OR-Tools 依赖");
            return;
        }

        solver.enableOutput();
        // ========== 4. 定义决策变量 ==========

        // x[i][j][t]: 产品i,在j产线，第0-t天生产的数量（连续变量，≥0）

        MPVariable[][][] x = new MPVariable[numProducts][numLines][numDays];
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    x[i][j][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
                            "x_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                }
            }
        }




        // I[i][t]: 产品i第0-t天末的库存量（连续变量，≥0）
        MPVariable[][] inventory = new MPVariable[numProducts][numDays];
        for (int i = 0; i < numProducts; i++) {
            for (int t = 0; t < numDays; t++) {
                inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "I_" + products[i] + "_d" + (t + 1));
            }
        }

        // y[i][j][t]: 第t天产线j是否生产产品i（0-1变量）
        MPVariable[][][] y = new MPVariable[numProducts][numLines][numDays];
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    y[i][j][t] = solver.makeBoolVar(
                            "y_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                }
            }
        }

        // shortfall[i][t]: 产品i第t天未满足的需求量（≥0），解决互斥约束下某产品产量=0时等式不成立的问题
        MPVariable[][] shortfall = new MPVariable[numProducts][numDays];
        for (int i = 0; i < numProducts; i++) {

                for (int t = 0; t < numDays; t++) {
                    shortfall[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "short_" + products[i] + "_d" + (t + 1));
                }


        }

        // ========== 换型相关变量（支持任意数量产品）==========

        // s[i][k][j][t]: 产线j第t天的第k个位置是否生产产品i（0-1变量）
        // k=0表示第一个位置，k=1表示第二个位置，以此类推
        MPVariable[][][][] s = new MPVariable[numProducts][maxProductsPerDay][numLines][numDays];
        for (int i = 0; i < numProducts; i++) {
            for (int k = 0; k < maxProductsPerDay; k++) {
                for (int j = 0; j < numLines; j++) {
                    for (int t = 0; t < numDays; t++) {
                        s[i][k][j][t] = solver.makeBoolVar(
                                "s_" + products[i] + "_pos" + (k + 1) + "_" + lines[j] + "_d" + (t + 1));
                        //s_P1_pos1_L1_d1,s_P1_pos1_L1_d2
                        //s_P1_pos2_L1_d1,s_P1_pos2_L1_d2
                        //s_P1_pos3_L1_d1,s_P1_pos3_L1_d2
                        //s_P2_pos1_L1_d1,s_P2_pos1_L1_d2
                        //s_P2_pos2_L1_d1,s_P2_pos2_L1_d2
                        //s_P2_pos3_L1_d1,s_P2_pos3_L1_d2
                        //s_P3_pos1_L1_d1,s_P3_pos1_L1_d2
                        //s_P3_pos2_L1_d1,s_P3_pos2_L1_d2
                        //s_P3_pos3_L1_d1,s_P3_pos3_L1_d2
                        //s_P1_pos2_L2_d1,s_P1_pos2_L2_d2
                        //s_P1_pos3_L2_d1,s_P1_pos3_L2_d2
                    }
                }
            }
        }

        // lastProduct[i][j][t]: 产线j第t天最后生产的产品是否是i（0-1变量）
        // 只有当产品i在某个位置k，且位置k+1没有产品时，lastProduct[i][j][t] = 1
        MPVariable[][][] lastProduct = new MPVariable[numProducts][numLines][numDays];
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    lastProduct[i][j][t] = solver.makeBoolVar(
                            "last_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    //last_P1_L1_d1,last_P1_L1_d2
                    //last_P2_L1_d1,last_P2_L1_d2
                    //last_P3_L1_d1,last_P3_L1_d2
                }
            }
        }

        // switchTo[i][j][t]: 产线j第t天是否切换到产品i（包括同一天切换和跨天切换）（0-1变量）
        MPVariable[][][] switchTo = new MPVariable[numProducts][numLines][numDays];
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    switchTo[i][j][t] = solver.makeBoolVar(
                            "switchTo_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    //switchTo_P1_L1_d1,switchTo_P1_L1_d2
                    //switchTo_P2_L1_d1,switchTo_P2_L1_d2
                    //switchTo_P3_L1_d1,switchTo_P3_L1_d2
                }
            }
        }



        // ========== 5. 目标函数：最小化总成本 ==========
        MPObjective objective = solver.objective();

        // 单位生产成本 c[i][j]（元/件）
        double[][] unitCost = {
                {5.0, 4.0},   // P1: L1=5元, L2=4元
                {8.0, 7.0},    // P2: L1=8元, L2=7元
                {6.0, 5.0}    // P3: L1=6元, L2=5元

        };        // 生产成本
        // 生产成本
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    objective.setCoefficient(x[i][j][t], unitCost[i][j]);
                }
            }
        }

        // 单位库存持有成本 h[i]（元/件/天）
        double[] holdingCost = {0.5, 1.0, 0.8};
        // 库存持有成本
        for (int i = 0; i < numProducts; i++) {
            for (int t = 0; t < numDays; t++) {
                objective.setCoefficient(inventory[i][t], holdingCost[i]);
            }
        }
        // ---- 换型成本 s[i][j]：产线j切换到产品i的换型成本（元/次）----
        double[][] setupCost = {
                {100, 80},    // P1: L1换型100元, L2换型80元
                {150, 120},// P2: L1换型150元, L2换型120元
                {120, 90}     // P3: L1换型120元, L2换型90元

        };        // 换型成本

        // 换型成本（每条产线分别计算）
        // 只有从一种产品切换到另一种产品时才产生换型成本
        // 换型成本：只有切换到产品i时才产生换型成本
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    objective.setCoefficient(switchTo[i][j][t], setupCost[i][j]);
                }
            }
        }

        // 缺口惩罚（元/件），权重最大，优先满足需求
        double shortfallPenalty = 1000;
        for (int i = 0; i < numProducts; i++) {
            for (int t = 0; t < numDays; t++) {
                objective.setCoefficient(shortfall[i][t], shortfallPenalty);
            }
        }
        objective.setMinimization();

        // ========== 6. 添加约束 ==========
        // 需求量 D[i][t]：产品i在第t天的需求
        double[][] demand = {
                {80, 60},   // P1: 第1天80, 第2天60
                {50, 70},    // P2: 第1天50, 第2天70
                {30, 40}    // P3: 第1天30, 第2天40
        };
        // 初始库存
        double[] initialInventory = {20, 10, 5};

        // ---- 约束1：库存平衡（加入shortfall吸收缺口）----
        // 公式：x + shortfall − inventory = demand − startInventory
        // 第1天：startInventory = initialInventory
        //上下界相等 → 等式约束
        // 第1天：各个产线当天产量+产品缺口-期末库存=当天需求-初始库存
        //当天产量-期末库存=当天需求-初始库存
        //当天产量=当天需求-初始库存+期末库存

        for (int i = 0; i < numProducts; i++) {

            MPConstraint invDay1 = solver.makeConstraint(
                    demand[i][0] - initialInventory[i],
                    demand[i][0] - initialInventory[i],
                    "inv_balance_" + products[i] + "_d1");
            for (int j = 0; j < numLines; j++) {
                invDay1.setCoefficient(x[i][j][0], 1);
            }
            invDay1.setCoefficient(shortfall[i][0], 1);   // 缺口补等式
            invDay1.setCoefficient(inventory[i][0], -1);

        }

        // 第2天及以后：上期库存 + 各产线当天产量的和 + 缺口 = 当天需求 + 期末库存
        // 第2天及以后：startInventory = 上期期末库存
        // 公式：inventory[i][t-1] + x[i][t] + shortfall[i][t] = demand[i][t] + inventory[i][t]
        // 移项后：inventory[i][t-1] + x[i][t] + shortfall[i][t] - inventory[i][t] = demand[i][t]

        for (int i = 0; i < numProducts; i++) {
            for (int t = 1; t < numDays; t++) {
                MPConstraint inv = solver.makeConstraint(
                        demand[i][t], demand[i][t],
                        "inv_balance_" + products[i] + "_d" + (t + 1));
                inv.setCoefficient(inventory[i][t - 1], 1);  // 上期库存
                for (int j = 0; j < numLines; j++) {
                    inv.setCoefficient(x[i][j][t], 1);//各产线当天产量
                }
                inv.setCoefficient(shortfall[i][t], 1);      // 缺口补等式
                inv.setCoefficient(inventory[i][t], -1);     // 期末库存
            }
        }

        // ---- 约束2：安全库存约束 ----
        // 安全库存
        double safetyStock = 10;
        for (int i = 0; i < numProducts; i++) {
            for (int t = 0; t < numDays; t++) {
                MPConstraint ss = solver.makeConstraint(safetyStock, Double.POSITIVE_INFINITY, "safety_stock_" + products[i] + "_d" + (t + 1));
                ss.setCoefficient(inventory[i][t], 1);
                // 期末库存=安全库存
            }
        }

        // ---- 约束3：产能约束 ----


        // ---- 生产效率 p[i][j]：产线j生产产品i的效率（件/小时）----
        double[][] productivity = {
                {20, 15},     // P1: L1=20件/时, L2=15件/时
                {10, 8},       // P2: L1=10件/时, L2=8件/时
                {15, 12}      // P3: L1=15件/时, L2=12件/时
        };
        // ---- 每条产线每日产能（小时）----
        double[] dailyCapacity = {10, 10};  // L1=10小时, L2=10小时

// ---- 换型时间 setupTime[i][j]：产线j切换到产品i所需时间（小时）----
        double[][] setupTime = {
                {0.5, 0.4},   // P1: L1换型0.5小时, L2换型0.4小时
                {0.8, 0.6},   // P2: L1换型0.8小时, L2换型0.6小时
                {0.6, 0.5}    // P3: L1换型0.6小时, L2换型0.5小时
        };

        // ---- 约束3：产能约束（每条产线独立计算）----
            //当天产量<=生产效率*每日产能
        // 各产品产量/效率之和 + 换型时间消耗 ≤ 日产能

        for (int j = 0; j < numLines; j++) {
            for (int t = 0; t < numDays; t++) {
                MPConstraint cap = solver.makeConstraint(0, dailyCapacity[j],
                        "capacity_" + lines[j] + "_d" + (t + 1));
                for (int i = 0; i < numProducts; i++) {
                    cap.setCoefficient(x[i][j][t], 1.0 / productivity[i][j]);
                    cap.setCoefficient(switchTo[i][j][t], setupTime[i][j]);
                }
            }
        }
        // ---- 大M常数 ----
        double bigM = 10000;

        // ---- 约束4：生产开关约束（大M法，每条产线独立）----
        // x[i][j][t] <= M * y[i][j][t]
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    MPConstraint sw = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
                            "switch_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    sw.setCoefficient(x[i][j][t], 1);
                    sw.setCoefficient(y[i][j][t], -bigM);
                }
            }
        }

        //约束5--影响y>约束4---影响y--影响x> x=0的话约束1不成立 初始库存(20) + 当天产量(0) = 当天需求(80) + 期末库存(10) 不成立无解

        // ============================================================================
        // 换型顺序相关约束（支持任意数量产品）
        //
        // 【核心思想】使用位置变量 s[i][k][j][t] 追踪产品在产线上的生产顺序
        //   - s[i][k][j][t] = 1: 产线j第t天的第k个位置生产产品i
        //   - k=0 表示第一个位置，k=1 表示第二个位置，以此类推
        //
        // 【数据实例】假设某天L1生产顺序为 P2 → P1 → P3：
        //   s[P2][0][L1][day] = 1  (P2在第一个位置)
        //   s[P1][1][L1][day] = 1  (P1在第二个位置)
        //   s[P3][2][L1][day] = 1  (P3在第三个位置)
        //   其他 s[i][k][L1][day] = 0
        //
        // 【换型成本计算规则】
        //   1. 第一个位置的产品 (k=0) 不收取换型成本（当天首次生产）
        //   2. 位置 k>0 的产品收取换型成本（当天切换）
        //   3. 如果前一天最后一个产品 ≠ 当天第一个产品，收取跨天换型成本
        //
        // 【数据实例】假设Day1 L1生产P2，Day2 L1生产P3 → P1：
        //   Day1: lastProduct[P2][L1][d1] = 1 (P2是最后产品)
        //   Day2: s[P3][0][L1][d2] = 1 (P3是第一个产品)
        //         因为 P2 ≠ P3，所以 switchTo[P3][L1][d2] = 1 (跨天切换)
        //         因为 P1在位置1>0，所以 switchTo[P1][L1][d2] = 1 (当天切换)
        // ============================================================================
        // ---- 约束5：位置变量s的约束 ----
        // 每个位置k最多分配一个产品
        // 【数据实例】位置k=0可以生产P1、P2或P3中的任意一个，但只能选一个
        //   s[P1][0] + s[P2][0] + s[P3][0] ≤ 1
        for (int j = 0; j < numLines; j++) {
            for (int t = 0; t < numDays; t++) {
                for (int k = 0; k < maxProductsPerDay; k++) {
                    MPConstraint posOne = solver.makeConstraint(0, 1,
                            "pos_one_" + lines[j] + "_d" + (t + 1) + "_pos" + (k + 1));
                    for (int i = 0; i < numProducts; i++) {
                        posOne.setCoefficient(s[i][k][j][t], 1);
                        //0<=s_P1_pos1_L1_d1,s_P2_pos1_L1_d1,s_P3_pos1_L1_d1<=1,最好只能生产一个产品
                        //0<=s_P1_pos2_L1_d1,s_P2_pos2_L1_d1,s_P3_pos2_L1_d1<=1,

                    }
                }
            }
        }
        // 5.2 每个产品在当天最多占用一个位置（如果生产了该产品）
        // 【数据实例】如果L1生产P2，那么P2只能在位置0、1、2中的一个：
        //   s[P2][0] + s[P2][1] + s[P2][2] ≤ 1
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    MPConstraint prodOne = solver.makeConstraint(0, 1,
                            "prod_one_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    for (int k = 0; k < maxProductsPerDay; k++) {
                        prodOne.setCoefficient(s[i][k][j][t], 1);
                        //0<=s_P1_pos1_L1_d1,s_P1_pos2_L1_d1,s_P1_pos3_L1_d1<=1一个产品一个产线一天最多生产一次

                    }
                }
            }
        }


        // 5.3 如果产品i在当天被生产(y[i]=1)，则必须分配到某个位置
        // 【数据实例】如果 y[P2][L1][d1] = 1，那么：
        //   s[P2][0] + s[P2][1] + s[P2][2] = 1
        //   （P2必须在某个位置生产）
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    MPConstraint prodPos = solver.makeConstraint(0, 0,
                            "prod_pos_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    for (int k = 0; k < maxProductsPerDay; k++) {
                        prodPos.setCoefficient(s[i][k][j][t], 1);
                    }
                    prodPos.setCoefficient(y[i][j][t], -1);
                }
            }
        }
        // 5.4 位置必须连续：如果位置k被占用，则位置0到k-1也必须被占用
        // 【数据实例】如果生产顺序是 P2 → P1 → P3（3个产品）：
        //   位置0: s[P2][0]=1, s[P1][0]=0, s[P3][0]=0, sum=1
        //   位置1: s[P2][1]=0, s[P1][1]=1, s[P3][1]=0, sum=1
        //   位置2: s[P2][2]=0, s[P1][2]=0, s[P3][2]=1, sum=1
        //   约束要求: sum(pos1) ≤ sum(pos0) → 1 ≤ 1 ✓
        //            sum(pos2) ≤ sum(pos1) → 1 ≤ 1 ✓
        //   反例：如果位置0没有产品，但位置1有产品，这是不允许的
        for (int j = 0; j < numLines; j++) {
            for (int t = 0; t < numDays; t++) {
                for (int k = 1; k < maxProductsPerDay; k++) {
                    // sum(s[i][k]) <= sum(s[i][k-1])
                    // 如果位置k有产品，则位置k-1必须有产品
                    MPConstraint cont = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
                            "cont_" + lines[j] + "_d" + (t + 1) + "_pos" + (k + 1));
                    for (int i = 0; i < numProducts; i++) {
                        cont.setCoefficient(s[i][k][j][t], 1);
                        cont.setCoefficient(s[i][k - 1][j][t], -1);
                       // s_P1_pos2_L1_d1-s_P1_pos1_L1_d1=0;
                    }
                }
            }
        }

        // ---- 约束6：lastProduct变量的约束 ----
        // lastProduct[i][j][t] = 1 当且仅当产品i是产线j第t天最后生产的产品
        //
        // 【数据实例】生产顺序 P2 → P1 → P3：
        //   lastProduct[P2] = 0  (P2后面还有P1和P3)
        //   lastProduct[P1] = 0  (P1后面还有P3)
        //   lastProduct[P3] = 1  (P3是最后一个)
        //
        // 【数据实例】生产顺序 P2 → P1（只生产2个产品）：
        //   lastProduct[P2] = 0  (P2后面还有P1)
        //   lastProduct[P1] = 1  (P1是最后一个)
        //   lastProduct[P3] = 0  (P3根本没生产)
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    // 6.1 上界约束：lastProduct[i] ≤ sum(s[i][k])
                    // 只有生产了i才能成为lastProduct
                    // 【数据实例】如果P2没生产(sum(s[P2][k])=0)，则lastProduct[P2] ≤ 0

                    MPConstraint lastUpper = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
                            "last_upper_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    lastUpper.setCoefficient(lastProduct[i][j][t], 1);
                    for (int k = 0; k < maxProductsPerDay; k++) {
                        lastUpper.setCoefficient(s[i][k][j][t], -1);
                    }
                    //s_P1_pos1_L1_d1,s_P1_pos2_L1_d1,s_P1_pos3_L1_d1-lastProduct[P1][L1][d1]<=0

                    // 6.2 下界约束：lastProduct[i] ≥ s[i][k] - sum(s[all][k+1])
                    // 如果产品i在位置k，且位置k+1没有产品，则lastProduct[i]必须为1
                    // 【数据实例】生产顺序 P2 → P1 → P3：
                    //   对于P3在位置2: lastProduct[P3] ≥ s[P3][2] - 0 (k=2是最后位置)
                    //   对于P1在位置1: lastProduct[P1] ≥ s[P1][1] - sum(s[all][2]) = 1 - 1 = 0
                    //   对于P2在位置0: lastProduct[P2] ≥ s[P2][0] - sum(s[all][1]) = 1 - 1 = 0

                    for (int k = 0; k < maxProductsPerDay; k++) {
                        MPConstraint lastLower = solver.makeConstraint(0, Double.POSITIVE_INFINITY,
                                "last_lower_" + products[i] + "_" + lines[j] + "_d" + (t + 1) + "_pos" + k);
                        lastLower.setCoefficient(lastProduct[i][j][t], 1);
                        lastLower.setCoefficient(s[i][k][j][t], -1);
                        if (k < maxProductsPerDay - 1) {
                            for (int ii = 0; ii < numProducts; ii++) {
                                lastLower.setCoefficient(s[ii][k + 1][j][t], 1);
                            }
                        }
                        //lastProduct[P1][L1][d2]=P1_pos1_L1_d2-
                        // (P1_pos2_L1_d2+P1_pos3_L1_d2+
                        // P2_pos2_L1_d2+P2_pos3_L1_d2+
                        // P3_pos2_L1_d2+P3_pos3_L1_d2)
                    }

                    // 6.3 关键约束：如果产品i在位置k生产，且位置k+1有任何产品生产，则lastProduct[i]必须为0
                    // lastProduct[i] ≤ 2 - s[i][k] - sum(s[all][k+1])
                    // 当s[i][k]=1且sum(s[all][k+1])=1时：lastProduct[i] ≤ 0
                    // 当s[i][k]=1且sum(s[all][k+1])=0时：lastProduct[i] ≤ 1（允许是最后产品）
                    // 【数据实例1】只生产P2（s[P2][0]=1, sum(s[all][1])=0）：
                    //   lastProduct[P2] ≤ 2 - 1 - 0 = 1 ✓（P2可以是最后产品）
                    // 【数据实例2】生产顺序 P2 → P1 → P3：
                    //   对于P2在位置0: lastProduct[P2] ≤ 2 - 1 - sum(s[all][1]) = 2 - 1 - 1 = 0 ✓
                    //   对于P1在位置1: lastProduct[P1] ≤ 2 - 1 - sum(s[all][2]) = 2 - 1 - 1 = 0 ✓
                    for (int k = 0; k < maxProductsPerDay - 1; k++) {
                        MPConstraint lastBeforeNext = solver.makeConstraint(-Double.POSITIVE_INFINITY, 2,
                                "last_before_next_" + products[i] + "_" + lines[j] + "_d" + (t + 1) + "_pos" + k);
                        lastBeforeNext.setCoefficient(lastProduct[i][j][t], 1);
                        lastBeforeNext.setCoefficient(s[i][k][j][t], -1);
                        for (int ii = 0; ii < numProducts; ii++) {
                            lastBeforeNext.setCoefficient(s[ii][k + 1][j][t], -1);
                        }
                    }
                }
            }
        }

        // switchTo[i][j][t] = 1 表示需要切换到产品i（产生换型成本）
        //
        // 换型触发条件（满足任一即触发）：
        //   7.1 同一天切换：产品i在位置k > 0（不是第一个生产的）
        //   7.2 跨天切换：产品i在位置k=0（第一个），且前一天最后一个产品 ≠ i
        //
        // 【数据实例1 - 同一天切换】生产顺序 P2 → P1 → P3：
        //   switchTo[P2] = 0  (P2是第一个，不需要切换)
        //   switchTo[P1] = 1  (P1在位置1>0，需要切换)
        //   switchTo[P3] = 1  (P3在位置2>0，需要切换)
        //
        // 【数据实例2 - 跨天切换】Day1生产P2，Day2生产P3 → P1：
        //   Day1: lastProduct[P2] = 1
        //   Day2: switchTo[P3] = 1  (P3是第一个产品，但前一天最后产品是P2≠P3)
        //   Day2: switchTo[P1] = 1  (P1在位置1>0)
        //   Day2: switchTo[P2] = 0  (P2没生产)
        for (int i = 0; i < numProducts; i++) {
            for (int j = 0; j < numLines; j++) {
                for (int t = 0; t < numDays; t++) {
                    // 7.1 同一天切换到i：如果i在位置k > 0，则需要切换
                    // switchTo[i] >= s[i][k] 对于 k > 0
                    // 【数据实例】P1在位置1: switchTo[P1] ≥ s[P1][1] = 1
                    for (int k = 1; k < maxProductsPerDay; k++) {
                        MPConstraint stSame = solver.makeConstraint(0, Double.POSITIVE_INFINITY,
                                "switchTo_same_" + products[i] + "_" + lines[j] + "_d" + (t + 1) + "_pos" + k);
                        stSame.setCoefficient(switchTo[i][j][t], 1);
                        stSame.setCoefficient(s[i][k][j][t], -1);
                    }

                    // 7.2 跨天切换到i（第2天及以后）
                    if (t >= 1) {
                        // 如果i在当天的第一个位置(k=0)，且昨天最后生产的产品 ≠ i
                        // 使用lastProduct[other][j][t-1]精确识别昨天最后生产的产品
                        // 【数据实例】Day1 lastProduct[P2]=1，Day2 s[P3][0]=1:
                        //   因为P2≠P3，所以switchTo[P3] ≥ lastProduct[P2] + s[P3][0] - 1 = 1 + 1 - 1 = 1
                        for (int other = 0; other < numProducts; other++) {
                            if (other == i) continue;
                            // 如果other是昨天的最后一个产品，且i是今天的第一个产品，则需要切换
                            MPConstraint stCross = solver.makeConstraint(0, Double.POSITIVE_INFINITY,
                                    "switchTo_cross_" + products[i] + "_from_" + products[other] + "_" + lines[j] + "_d" + (t + 1));
                            stCross.setCoefficient(switchTo[i][j][t], 1);
                            stCross.setCoefficient(lastProduct[other][j][t - 1], -1);
                            stCross.setCoefficient(s[i][0][j][t], -1);
                        }
                    }

                    // 7.3 上界约束：switchTo[i] ≤ y[i]
                    // 只有生产了i才能切换到i（不生产就不需要切换）
                    // 【数据实例】如果y[P2]=0，则switchTo[P2] ≤ 0
                    MPConstraint stUpper = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
                            "switchTo_upper_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
                    stUpper.setCoefficient(switchTo[i][j][t], 1);
                    stUpper.setCoefficient(y[i][j][t], -1);
                }
            }
        }

        // ========== 7. 求解 ==========
        System.out.println("========== 开始求解（多产线MIP排产）==========");
        System.out.println("产线数量：" + numLines + "，产品数量：" + numProducts + "，天数：" + numDays);
        System.out.println();


        MPSolver.ResultStatus status = solver.solve();


        // ========== 8. 输出结果 ==========
        if (status == MPSolver.ResultStatus.OPTIMAL) {
            System.out.println("✅ 找到最优解！");
            System.out.printf("最小总成本：%.2f 元%n%n", objective.value());

            // 按天输出
            for (int t = 0; t < numDays; t++) {
                System.out.println("━━━━━━━━━━━━ 第 " + (t + 1) + " 天 ━━━━━━━━━━━━");

                // 每条产线的排产
                for (int j = 0; j < numLines; j++) {
                    System.out.println("  【" + lines[j] + "】");
                    double lineHours = 0;
                    double lineSetupHours = 0;
                    boolean produced = false;

                    // 确定生产顺序
                    String[] sequence = new String[maxProductsPerDay];
                    int seqLength = 0;
                    for (int k = 0; k < maxProductsPerDay; k++) {
                        for (int i = 0; i < numProducts; i++) {
                            if (s[i][k][j][t].solutionValue() > 0.001) {
                                sequence[seqLength++] = products[i];
                                break;
                            }
                        }
                    }

                    // 输出生产顺序
                    if (seqLength > 0) {
                        System.out.print("    生产顺序：");
                        for (int k = 0; k < seqLength; k++) {
                            if (k > 0) System.out.print(" → ");
                            System.out.print(sequence[k]);
                        }
                        System.out.println();

                        // 输出当天切换信息
                        for (int k = 0; k < seqLength - 1; k++) {
                            int fromIdx = -1, toIdx = -1;
                            for (int idx = 0; idx < numProducts; idx++) {
                                if (products[idx].equals(sequence[k])) fromIdx = idx;
                                if (products[idx].equals(sequence[k + 1])) toIdx = idx;
                            }
                            if (fromIdx >= 0 && toIdx >= 0) {
                                System.out.printf("    → 当天切换：%s → %s（换型成本+%.0f元，换型时间+%.1fh）%n",
                                        sequence[k], sequence[k + 1],
                                        setupCost[toIdx][j], setupTime[toIdx][j]);
                            }
                        }
                    }

                    // 输出跨天切换信息（第2天及以后）
                    if (t >= 1 && seqLength > 0) {
                        // 找到昨天的最后一个产品
                        String prevLastProd = "无";
                        for (int i = 0; i < numProducts; i++) {
                            if (lastProduct[i][j][t - 1].solutionValue() > 0.001) {
                                prevLastProd = products[i];
                                break;
                            }
                        }
                        if (!prevLastProd.equals("无") && !prevLastProd.equals(sequence[0])) {
                            int toIdx = -1;
                            for (int idx = 0; idx < numProducts; idx++) {
                                if (products[idx].equals(sequence[0])) toIdx = idx;
                            }
                            System.out.printf("    → 跨天切换：%s → %s（换型成本+%.0f元，换型时间+%.1fh）%n",
                                    prevLastProd, sequence[0],
                                    setupCost[toIdx][j], setupTime[toIdx][j]);
                        }
                    }

                    for (int i = 0; i < numProducts; i++) {
                        double qty = x[i][j][t].solutionValue();
                        double hours = qty / productivity[i][j];
                        double setupHr = switchTo[i][j][t].solutionValue() * setupTime[i][j];
                        lineHours += hours;
                        lineSetupHours += setupHr;
                        if (qty > 0.001) {
                            produced = true;
                            double cost = qty * unitCost[i][j];
                            double setupCostVal = switchTo[i][j][t].solutionValue() * setupCost[i][j];
                            System.out.printf("    → 生产 %s：%.0f 件，耗时 %.1fh，成本 %.0f 元",
                                    products[i], qty, hours, cost);
                            if (setupCostVal > 0) {
                                System.out.printf("（换型成本+%.0f，换型时间+%.1fh）", setupCostVal, setupHr);
                            }
                            System.out.println();
                        }
                    }
                    if (!produced) {
                        System.out.println("    （闲置）");
                    }
                    System.out.printf("    工时利用：生产%.1fh + 换型%.1fh = %.1f / %.1f 小时 (%.0f%%)%n",
                            lineHours, lineSetupHours, lineHours + lineSetupHours,
                            dailyCapacity[j], (lineHours + lineSetupHours) / dailyCapacity[j] * 100);
                    System.out.println();
                }

                // 库存
                System.out.println("  【期末库存】");
                for (int i = 0; i < numProducts; i++) {
                    System.out.printf("    %s：%.0f 件%n", products[i], inventory[i][t].solutionValue());
                }
                System.out.println();
            }

            // 成本明细
            double totalProdCost = 0, totalHoldCost = 0, totalSetupCost = 0, totalSetupTime = 0;
            for (int i = 0; i < numProducts; i++) {
                for (int j = 0; j < numLines; j++) {
                    for (int t = 0; t < numDays; t++) {
                        totalProdCost += x[i][j][t].solutionValue() * unitCost[i][j];
                        totalSetupCost += switchTo[i][j][t].solutionValue() * setupCost[i][j];
                        totalSetupTime += switchTo[i][j][t].solutionValue() * setupTime[i][j];
                    }
                }
            }
            for (int i = 0; i < numProducts; i++) {
                for (int t = 0; t < numDays; t++) {
                    totalHoldCost += inventory[i][t].solutionValue() * holdingCost[i];
                }
            }

            System.out.println("================ 成本明细 ================");
            System.out.printf("生产成本：  %8.2f 元%n", totalProdCost);
            System.out.printf("库存成本：  %8.2f 元%n", totalHoldCost);
            System.out.printf("换型成本：  %8.2f 元%n", totalSetupCost);
            System.out.printf("────────────────────────────%n");
            System.out.printf("总成本：    %8.2f 元%n", objective.value());

            // 换型时间统计
            System.out.println();
            System.out.println("============ 换型时间统计 ============");
            System.out.printf("  换型总耗时：%.1f 小时%n", totalSetupTime);


            // 产线利用率统计（包含换型时间）
            System.out.println();
            System.out.println("============ 产线利用率统计 ============");
            for (int j = 0; j < numLines; j++) {
                double totalProdHours = 0, totalSetupHours = 0;
                for (int t = 0; t < numDays; t++) {
                    for (int i = 0; i < numProducts; i++) {
                        totalProdHours += x[i][j][t].solutionValue() / productivity[i][j];
                        totalSetupHours += switchTo[i][j][t].solutionValue() * setupTime[i][j];
                    }
                }
                double totalHours = totalProdHours + totalSetupHours;
                double totalCap = dailyCapacity[j] * numDays;
                System.out.printf("  %s：生产%.1fh + 换型%.1fh = %.1f / %.1f 小时 (利用率 %.1f%%)%n",
                        lines[j], totalProdHours, totalSetupHours, totalHours, totalCap, totalHours / totalCap * 100);
            }

        } else if (status == MPSolver.ResultStatus.INFEASIBLE) {
            System.out.println("❌ 模型无解（INFEASIBLE）——产能不足，无法满足所有需求");
        } else {
            System.out.println("求解状态：" + status);
        }

        MPModelExportOptions options=new MPModelExportOptions();

        String lpText = solver.exportModelAsLpFormat(false);
       // FileHelper.writeFile(lpText,"model.lp");
        String mpsText = solver.exportModelAsMpsFormat(true,true);
      //  FileHelper.writeFile(mpsText,"model.mps");
        // 输出LP模型到控制台
        System.out.println("========== LP模型输出 ==========");
        // System.out.println(lpText);

        for(MPVariable var : solver.variables()){
            String name = var.name();

            double val = var.solutionValue();
            double lb = var.lb();
            double ub = var.ub();

            System.out.printf("变量[%s] 下界=%.2f 上界=%.2f 最优解=%.4f%n",
                    name, lb, ub, val);
        }

        // 求解统计
        System.out.println();
        System.out.println("============ 求解统计 ============");
        System.out.println("变量总数：" + solver.numVariables());
        System.out.println("约束总数：" + solver.numConstraints());
        System.out.printf("求解时间：%.3f 秒%n", solver.wallTime() / 1000.0);
    }
}
