返回博客

Google OR-Tools 深潜:coomia-dip 的约束求解与决策优化

1. [OR-Tools 在 coomia-dip 中的定位](#1-or-tools-在-coomia-dip-中的定位)

Coomia发布于 2025年11月27日11 分钟阅读
分享本文Twitter / X

系列:S8 技术组件深潜 · 第 18 篇 | 难度:高级 | 阅读时间:20 分钟

Google OR-Tools 深潜:coomia-dip 的约束求解与决策优化

#TL;DR

  • Google OR-Tools 是 coomia-dip Reasoning & Decision Layer(Reasoning & Decision Layer)的核心优化引擎,为资源分配、路径规划、排程调度等决策场景提供数学优化能力
  • 本文深入分析 OR-Tools 的四大求解器(LP/MIP/CP-SAT/Routing)、建模方法论、以及 coomia-dip 中 5 种典型优化场景的实现
  • 涵盖目标函数设计、约束建模、求解参数调优、解的质量评估、以及与 Ontology 数据模型的集成

#目录

  1. OR-Tools 在 coomia-dip 中的定位
  2. 四大求解器
  3. 线性规划与混合整数规划
  4. CP-SAT 约束规划
  5. 路径优化
  6. 排程调度
  7. 资源分配
  8. 与 Ontology 的集成
  9. 求解性能调优
  10. 生产部署模式
  11. Key Takeaways

#1. OR-Tools 在 coomia-dip 中的定位

#1.1 决策优化场景

场景求解器变量规模求解时间
供应链资源分配MIP10K-100K秒级
物流路径规划Routing100-10K 节点秒级
人员排程CP-SAT1K-50K秒-分钟
库存优化LP100K+毫秒级
设备维护计划CP-SAT1K-10K秒级

#1.2 架构集成

Code
┌────────────────────────────────────────┐
│        Reasoning & Decision Layer: Reasoning & Decision   │
│                                         │
│  ┌──────────────┐  ┌───────────────┐  │
│  │ Decision     │  │ Reasoning    │  │
│  │ Engine       │  │ Engine       │  │
│  └──────┬───────┘  └──────────────┘  │
│         │                             │
│  ┌──────┴───────────────────────┐    │
│  │    OR-Tools Solver Service    │    │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ │    │
│  │  │  LP  │ │CP-SAT│ │Route │ │    │
│  │  └──────┘ └──────┘ └──────┘ │    │
│  └──────────────────────────────┘    │
└────────────────────────────────────────┘

#2. 四大求解器

#2.1 求解器选择矩阵

求解器变量类型约束类型最优性速度
GLOP (LP)连续线性全局最优极快
SCIP (MIP)连续+整数线性全局最优
CP-SAT整数+布尔线性+逻辑全局最优
Routing整数路径约束近似最优

#3. 线性规划与混合整数规划

#3.1 资源分配 LP 示例

Python
from ortools.linear_solver import pywraplp

def optimize_resource_allocation(
    resources: list[dict],
    tasks: list[dict],
    costs: list[list[float]],
) -> dict:
    """资源分配优化:最小化总成本"""
    solver = pywraplp.Solver.CreateSolver("GLOP")

    n_resources = len(resources)
    n_tasks = len(tasks)

    # 决策变量:x[i][j] = 资源 i 分配给任务 j 的比例
    x = [[solver.NumVar(0, 1, f"x_{i}_{j}")
           for j in range(n_tasks)]
          for i in range(n_resources)]

    # 约束 1:每个任务必须被完全分配
    for j in range(n_tasks):
        solver.Add(sum(x[i][j] for i in range(n_resources)) == 1)

    # 约束 2:每个资源的总分配不超过其容量
    for i in range(n_resources):
        solver.Add(
            sum(x[i][j] * tasks[j]["demand"]
                for j in range(n_tasks))
            <= resources[i]["capacity"]
        )

    # 目标:最小化总成本
    solver.Minimize(
        sum(x[i][j] * costs[i][j]
            for i in range(n_resources)
            for j in range(n_tasks))
    )

    status = solver.Solve()

    if status == pywraplp.Solver.OPTIMAL:
        return {
            "status": "optimal",
            "total_cost": solver.Objective().Value(),
            "assignments": [
                {"resource": i, "task": j, "allocation": x[i][j].solution_value()}
                for i in range(n_resources)
                for j in range(n_tasks)
                if x[i][j].solution_value() > 0.01
            ],
        }
    return {"status": "infeasible"}

#3.2 MIP 示例:带固定成本的分配

Python
def optimize_facility_location(
    facilities: list[dict],
    customers: list[dict],
    fixed_costs: list[float],
    transport_costs: list[list[float]],
) -> dict:
    """设施选址:最小化固定成本 + 运输成本"""
    solver = pywraplp.Solver.CreateSolver("SCIP")

    n_facilities = len(facilities)
    n_customers = len(customers)

    # 整数变量:y[i] = 是否开设设施 i
    y = [solver.IntVar(0, 1, f"y_{i}") for i in range(n_facilities)]

    # 连续变量:x[i][j] = 设施 i 服务客户 j 的比例
    x = [[solver.NumVar(0, 1, f"x_{i}_{j}")
           for j in range(n_customers)]
          for i in range(n_facilities)]

    # 约束:每个客户被完全服务
    for j in range(n_customers):
        solver.Add(sum(x[i][j] for i in range(n_facilities)) == 1)

    # 约束:只有开设的设施才能服务
    for i in range(n_facilities):
        for j in range(n_customers):
            solver.Add(x[i][j] <= y[i])

    # 约束:设施容量
    for i in range(n_facilities):
        solver.Add(
            sum(x[i][j] * customers[j]["demand"]
                for j in range(n_customers))
            <= facilities[i]["capacity"] * y[i]
        )

    # 目标
    solver.Minimize(
        sum(fixed_costs[i] * y[i] for i in range(n_facilities))
        + sum(transport_costs[i][j] * x[i][j]
              for i in range(n_facilities)
              for j in range(n_customers))
    )

    status = solver.Solve()
    if status == pywraplp.Solver.OPTIMAL:
        opened = [i for i in range(n_facilities) if y[i].solution_value() > 0.5]
        return {"status": "optimal", "opened_facilities": opened,
                "total_cost": solver.Objective().Value()}
    return {"status": "infeasible"}

#4. CP-SAT 约束规划

#4.1 人员排程

Python
from ortools.sat.python import cp_model

def optimize_staff_scheduling(
    employees: list[dict],
    shifts: list[dict],
    days: int = 7,
) -> dict:
    """员工排班优化"""
    model = cp_model.CpModel()

    n_employees = len(employees)
    n_shifts = len(shifts)

    # 决策变量:schedule[e][d][s] = 员工 e 在第 d 天是否上班次 s
    schedule = {}
    for e in range(n_employees):
        for d in range(days):
            for s in range(n_shifts):
                schedule[(e, d, s)] = model.NewBoolVar(f"schedule_{e}_{d}_{s}")

    # 约束 1:每个班次需要最少人数
    for d in range(days):
        for s in range(n_shifts):
            model.Add(
                sum(schedule[(e, d, s)] for e in range(n_employees))
                >= shifts[s]["min_staff"]
            )

    # 约束 2:每人每天最多一个班次
    for e in range(n_employees):
        for d in range(days):
            model.Add(
                sum(schedule[(e, d, s)] for s in range(n_shifts)) <= 1
            )

    # 约束 3:每周工作天数限制
    for e in range(n_employees):
        model.Add(
            sum(schedule[(e, d, s)]
                for d in range(days) for s in range(n_shifts))
            <= employees[e].get("max_days", 5)
        )

    # 约束 4:不能连续上夜班超过 2 天
    night_shift = n_shifts - 1
    for e in range(n_employees):
        for d in range(days - 2):
            model.Add(
                schedule[(e, d, night_shift)]
                + schedule[(e, d + 1, night_shift)]
                + schedule[(e, d + 2, night_shift)]
                <= 2
            )

    # 目标:最大化员工偏好满足度
    preferences = []
    for e in range(n_employees):
        for d in range(days):
            for s in range(n_shifts):
                pref = employees[e].get("preferences", {}).get(f"{d}_{s}", 0)
                preferences.append(schedule[(e, d, s)] * pref)
    model.Maximize(sum(preferences))

    # 求解
    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = 30
    solver.parameters.num_workers = 4

    status = solver.Solve(model)

    if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
        result = []
        for e in range(n_employees):
            for d in range(days):
                for s in range(n_shifts):
                    if solver.Value(schedule[(e, d, s)]) == 1:
                        result.append({
                            "employee": employees[e]["name"],
                            "day": d,
                            "shift": shifts[s]["name"],
                        })
        return {"status": "optimal" if status == cp_model.OPTIMAL else "feasible",
                "schedule": result, "objective": solver.ObjectiveValue()}
    return {"status": "infeasible"}

#5. 路径优化

#5.1 车辆路径问题(VRP)

Python
from ortools.constraint_solver import routing_enums_pb2, pywrapcp

def optimize_vehicle_routing(
    depot: int,
    locations: list[tuple[float, float]],
    demands: list[int],
    vehicle_capacities: list[int],
    num_vehicles: int,
) -> dict:
    """带容量约束的车辆路径优化"""
    n = len(locations)

    # 创建距离矩阵
    distance_matrix = compute_distance_matrix(locations)

    manager = pywrapcp.RoutingIndexManager(n, num_vehicles, depot)
    routing = pywrapcp.RoutingModel(manager)

    # 距离回调
    def distance_callback(from_index, to_index):
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return int(distance_matrix[from_node][to_node])

    transit_callback_index = routing.RegisterTransitCallback(distance_callback)
    routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

    # 容量约束
    def demand_callback(from_index):
        from_node = manager.IndexToNode(from_index)
        return demands[from_node]

    demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
    routing.AddDimensionWithVehicleCapacity(
        demand_callback_index, 0, vehicle_capacities, True, "Capacity"
    )

    # 求解参数
    search_parameters = pywrapcp.DefaultRoutingSearchParameters()
    search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
    )
    search_parameters.local_search_metaheuristic = (
        routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
    )
    search_parameters.time_limit.seconds = 30

    solution = routing.SolveWithParameters(search_parameters)

    if solution:
        routes = []
        for v in range(num_vehicles):
            route = []
            index = routing.Start(v)
            while not routing.IsEnd(index):
                route.append(manager.IndexToNode(index))
                index = solution.Value(routing.NextVar(index))
            route.append(manager.IndexToNode(index))
            routes.append(route)
        return {"status": "solved", "routes": routes,
                "total_distance": solution.ObjectiveValue()}
    return {"status": "no_solution"}

#6. 排程调度

#6.1 Job Shop 调度

Python
def optimize_job_shop(
    jobs: list[list[tuple[int, int]]],  # [(machine, duration), ...]
    num_machines: int,
) -> dict:
    """Job Shop 调度优化:最小化 Makespan"""
    model = cp_model.CpModel()

    horizon = sum(d for job in jobs for _, d in job)
    all_tasks = {}

    for job_id, job in enumerate(jobs):
        for task_id, (machine, duration) in enumerate(job):
            start = model.NewIntVar(0, horizon, f"start_{job_id}_{task_id}")
            end = model.NewIntVar(0, horizon, f"end_{job_id}_{task_id}")
            interval = model.NewIntervalVar(start, duration, end,
                                            f"interval_{job_id}_{task_id}")
            all_tasks[(job_id, task_id)] = (start, end, interval, machine, duration)

    # 约束:同一 Job 内的 Task 顺序执行
    for job_id, job in enumerate(jobs):
        for task_id in range(len(job) - 1):
            model.Add(all_tasks[(job_id, task_id + 1)][0]
                      >= all_tasks[(job_id, task_id)][1])

    # 约束:同一 Machine 上的 Task 不重叠
    for m in range(num_machines):
        machine_intervals = [
            all_tasks[key][2]
            for key in all_tasks
            if all_tasks[key][3] == m
        ]
        model.AddNoOverlap(machine_intervals)

    # 目标:最小化 Makespan
    makespan = model.NewIntVar(0, horizon, "makespan")
    model.AddMaxEquality(makespan, [
        all_tasks[key][1] for key in all_tasks
    ])
    model.Minimize(makespan)

    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = 60
    status = solver.Solve(model)

    if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
        schedule = []
        for key, (start, end, _, machine, duration) in all_tasks.items():
            schedule.append({
                "job": key[0], "task": key[1],
                "machine": machine, "start": solver.Value(start),
                "end": solver.Value(end),
            })
        return {"status": "optimal" if status == cp_model.OPTIMAL else "feasible",
                "makespan": solver.Value(makespan), "schedule": schedule}
    return {"status": "infeasible"}

#7. 资源分配

Python
def optimize_budget_allocation(
    projects: list[dict],
    total_budget: float,
    min_allocation: float = 0,
) -> dict:
    """预算分配优化:最大化加权收益"""
    solver = pywraplp.Solver.CreateSolver("GLOP")

    n = len(projects)
    x = [solver.NumVar(min_allocation, total_budget, f"x_{i}") for i in range(n)]

    # 预算约束
    solver.Add(sum(x) <= total_budget)

    # 项目最低/最高约束
    for i, project in enumerate(projects):
        if "min_budget" in project:
            solver.Add(x[i] >= project["min_budget"])
        if "max_budget" in project:
            solver.Add(x[i] <= project["max_budget"])

    # 目标:最大化加权 ROI
    solver.Maximize(sum(x[i] * projects[i]["expected_roi"] for i in range(n)))

    status = solver.Solve()
    if status == pywraplp.Solver.OPTIMAL:
        return {"status": "optimal",
                "allocations": {projects[i]["name"]: x[i].solution_value() for i in range(n)},
                "total_expected_return": solver.Objective().Value()}
    return {"status": "infeasible"}

#8. 与 Ontology 的集成

Python
class OntologyOptimizationService:
    """将 Ontology 数据模型与 OR-Tools 求解器集成"""

    async def optimize(self, world_id: str, problem_type: str, params: dict) -> dict:
        # 1. 从 Ontology 加载数据
        data = await self.load_ontology_data(world_id, problem_type)

        # 2. 构建优化模型
        solver_map = {
            "resource_allocation": optimize_resource_allocation,
            "vehicle_routing": optimize_vehicle_routing,
            "staff_scheduling": optimize_staff_scheduling,
            "job_shop": optimize_job_shop,
            "budget_allocation": optimize_budget_allocation,
        }
        solver_func = solver_map[problem_type]

        # 3. 求解(CPU 密集,在线程池中执行)
        result = await asyncio.get_event_loop().run_in_executor(
            self.executor, solver_func, **data, **params
        )

        # 4. 将结果写回 Ontology
        await self.store_decision_result(world_id, problem_type, result)

        return result

#9. 求解性能调优

参数说明推荐值
max_time_in_seconds求解时间上限10-60s
num_workers并行求解线程CPU 核数
log_search_progress输出求解日志开发时 True
relative_gap_limit相对间隙(MIP)0.01 (1%)
enumerate_all_solutions枚举所有解False

#10. 生产部署模式

Python
# 异步包装 OR-Tools(CPU 密集型)
class SolverService:
    def __init__(self, max_workers: int = 4):
        self.executor = ProcessPoolExecutor(max_workers=max_workers)

    async def solve(self, problem: OptimizationProblem) -> SolverResult:
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(
            self.executor, self._sync_solve, problem
        )

    def _sync_solve(self, problem: OptimizationProblem) -> SolverResult:
        # 在独立进程中执行,避免 GIL 影响
        solver = create_solver(problem)
        return solver.solve()

#11. Key Takeaways

主题关键结论
求解器选择LP 连续 / MIP 整数 / CP-SAT 逻辑 / Routing 路径
建模先定义变量和约束,最后设目标函数
性能设置时间上限,接受次优解
集成从 Ontology 加载数据,结果写回
部署ProcessPoolExecutor 避免 GIL
CP-SAT最灵活,支持逻辑约束和 NoOverlap
VRP使用 Guided Local Search 元启发式

下一篇预告:S8-19 将深入 Trino 查询联邦,探讨 coomia-dip 如何实现跨引擎统一查询。