You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Docplex自定义回调实现子问题求解与割平面添加问题求助

Docplex自定义回调实现子问题求解与割平面添加问题求助

我现在正尝试实现一个自定义回调函数,在主模型的优化过程中求解一个子问题,具体想完成这几个步骤:

  • 从主模型的当前解中获取特定变量的取值
  • 基于这些值求解一个独立的子优化问题
  • 如果子问题的解提示需要添加割平面,就把这个割平面作为新约束加入主模型

我想用Python实现,但现在完全摸不着头绪,也试过Xpress之类的工具,但文档对我帮助不大。下面是我写的代码,真心希望能得到大家的帮助!

from docplex.mp.model import Model
from cplex.callbacks import UserCutCallback
from docplex.mp.callbacks.cb_mixin import ConstraintCallbackMixin
import random

class CustomCutCallback(ConstraintCallbackMixin, UserCutCallback):
    def __init__(self, env):
        UserCutCallback.__init__(self, env)
        ConstraintCallbackMixin.__init__(self)
        self.eps = 1e-6
        self.nb_cuts = 0
        self.cts = []

    def add_cut_constraint(self, cut):
        self.register_constraint(cut)

    def __call__(self, context):
        """
        This method is invoked by CPLEX during optimization.
        It receives the 'context' to interact with the model and variables.
        """
        print("Callback called with context")
        m = context.model  # The main model from the context
        m_sol = m.solution

        x_values = {var: m_sol.get_value(var) for var in m.variables if var.name.startswith('x')}
        w_values = {var: m_sol.get_value(var) for var in m.variables if var.name.startswith('w')}
        m2_cuts = self.solve_subproblem(x_values, w_values, m, context)

    def solve_subproblem(self, x_values, w_values, m, context):
        """
        Solves the subproblem and generates cuts if necessary.
        """
        m2 = Model(name='subproblem')

        client_range = range(len(x_values))
        deposit_range = range(len(w_values))
        plant_range = range(len(w_values[0]))

        alpha = m2.continuous_var_matrix(client_range, deposit_range, name='alpha', lb=0)
        beta = m2.continuous_var_matrix(deposit_range, plant_range, name='beta', lb=0)

        m2.add_constraints(alpha[i, j] + (x_values.get((i, j), 0) * beta[j, k]) <= x_values.get((i, j), 0) * w_values.get((j, k), 0)
                           for i in client_range for j in deposit_range for k in plant_range)

        m2.maximize(m2.sum(alpha[i, j] * x_values.get((i, j), 0) for i in client_range for j in deposit_range) + 
                    m2.sum(beta[j, k] * w_values.get((j, k), 0) for j in deposit_range for k in plant_range))

        m2.solve()
        print(m2.solution)

        # Here, you perform an evaluation of the cut values
        for i in client_range:
            for j in deposit_range:
                for k in plant_range:
                    if  sum(m2.solution.get_value(alpha[i, j]) * x_values.get((i, j), 0) for i in client_range for j in deposit_range) + \
                    sum(m2.solution.get_value(beta[j, k]) * w_values.get((j, k), 0) for j in deposit_range for k in plant_range) > \
                    sum(transport_cost_deposit_client[j][i] * d[i] * x[i, j] for i in client_range for j in deposit_range) + \
                    sum(transport_cost_plant_deposit[j][k] * w[j, k] for j in deposit_range for k in plant_range):
                        m.add_constraint(sum(m2.solution.get_value(alpha[i, j]) * x[i, j] for i in client_range for j in deposit_range) + \
                                         sum(m2.solution.get_value(beta[j, k]) * w[j, k] for j in deposit_range for k in plant_range))


# Main model
def build_location_model(transport_cost_deposit_client, transport_cost_plant_deposit, p, q, d, **kwargs):
    m = Model(name='location', **kwargs)

    num_deposits = len(transport_cost_deposit_client)
    num_plants = len(transport_cost_plant_deposit[0])
    num_clients = len(d)
    
    deposit_range = range(num_deposits)
    plant_range = range(num_plants)
    client_range = range(num_clients)

    x = m.binary_var_matrix(client_range, deposit_range, name='x')
    w = m.integer_var_matrix(deposit_range, plant_range, name='w', lb=0)
    y = m.binary_var_list(deposit_range, name='y')
    h = m.binary_var_list(plant_range, name='h')

    m.add_constraints(m.sum(x[i, j] for j in deposit_range) == 1 for i in client_range)
    m.add_constraints(m.sum(w[j, k] for k in plant_range) == y[j] for j in deposit_range)
    m.add_constraints(x[i, j] <= y[j] for i in client_range for j in deposit_range)
    m.add_constraint(m.sum(h[k] for k in plant_range) == q)
    m.add_constraint(m.sum(y[j] for j in deposit_range) == p)
    m.add_constraints(m.sum(d[i] * x[i,j] for i in client_range) == m.sum(w[j, k] for k in plant_range) for j in deposit_range)
    m.add_constraints(w[j, k] <= (m.sum(d[i] for i in client_range) * h[k]) for j in deposit_range for k in plant_range)

    transport_cost = m.sum(transport_cost_deposit_client[j][i] * d[i] * x[i, j] for i in client_range for j in deposit_range) + \
                        m.sum(transport_cost_plant_deposit[j][k] * w[j, k] for j in deposit_range for k in plant_range)
    m.minimize(transport_cost)
    m.parameters.preprocessing.presolve = 0

    # Register the callback
    cut_cb = m.register_callback(CustomCutCallback)

    # Configure CPLEX parameters for cuts
    params = m.parameters
    params.mip.cuts.mircut = -1

    m.solve()

    return m



# Test function
def solve_model():
    num_deposits = 10
    num_plants = 4
    num_clients = 20

    TRANSPORT_COST_DEPOSITS_CLIENTS = [
        [random.randint(20, 100) for _ in range(num_clients)] for _ in range(num_deposits)
    ]

    TRANSPORT_COST_PLANTS_DEPOSITS = [
        [random.randint(30, 80) for _ in range(num_plants)] for _ in range(num_deposits)
    ]

    p = 5
    q = 3
    d = [random.randint(5, 20) for _ in range(num_clients)]

    m = build_location_model(TRANSPORT_COST_DEPOSITS_CLIENTS, TRANSPORT_COST_PLANTS_DEPOSITS, p, q, d)
    if m.solution is None:
        print("No valid solution found.")
        return None
    print(m.solution)
    return m


if __name__ == "__main__":
    solve_model()

针对你代码的问题分析与修复建议

1. 回调中获取变量值的正确方式

你当前用m.solution.get_value(var)获取的是全局最优解(如果存在的话),但回调触发时我们需要的是当前MIP节点的松弛解,应该用context.get_values(var)来获取:

# 更高效的方式:提前把主模型的x、w变量传给回调,避免遍历所有变量
x_vals = {(i,j): context.get_values(self.x_vars[i,j]) for i,j in self.x_vars}

2. 子问题维度匹配错误

你用len(x_values)来推导client_range是错误的——x_values是变量字典,长度是x变量的总数,不是客户数量。建议在回调初始化时,直接传入主模型的client_range、deposit_range、plant_range以及x、w变量矩阵,避免动态推导出错。

3. 割平面添加的错误逻辑

在回调中不能直接调用m.add_constraint()修改主模型,必须用ConstraintCallbackMixin提供的register_constraint方法。同时你代码中transport_cost_deposit_client等参数在回调中未定义,需要提前传入回调。

4. 子问题性能优化

每次回调都重建子模型会非常低效,建议提前初始化子模型结构,每次仅更新约束系数和目标函数即可。


调整后的核心回调代码示例

class CustomCutCallback(ConstraintCallbackMixin, UserCutCallback):
    def __init__(self, env, main_model, x_vars, w_vars, client_range, deposit_range, plant_range, tc_dc, tc_pd, d):
        UserCutCallback.__init__(self, env)
        ConstraintCallbackMixin.__init__(self)
        self.eps = 1e-6
        self.nb_cuts = 0
        self.main_model = main_model
        self.x_vars = x_vars  # 主模型x[i,j]变量矩阵
        self.w_vars = w_vars  # 主模型w[j,k]变量矩阵
        self.client_range = client_range
        self.deposit_range = deposit_range
        self.plant_range = plant_range
        self.tc_dc = tc_dc
        self.tc_pd = tc_pd
        self.d = d
        # 提前初始化子问题
        self._init_subproblem()

    def _init_subproblem(self):
        self.sub_model = Model(name='subproblem')
        self.alpha = self.sub_model.continuous_var_matrix(self.client_range, self.deposit_range, name='alpha', lb=0)
        self.beta = self.sub_model.continuous_var_matrix(self.deposit_range, self.plant_range, name='beta', lb=0)
        # 初始化空约束,后续更新系数
        self.sub_constraints = []
        for i in self.client_range:
            for j in self.deposit_range:
                for k in self.plant_range:
                    self.sub_constraints.append(
                        self.sub_model.add_constraint(self.alpha[i,j] + 0 <= 0)
                    )

    def _update_subproblem(self, x_vals, w_vals):
        # 更新子问题约束与目标
        idx = 0
        for i in self.client_range:
            for j in self.deposit_range:
                for k in self.plant_range:
                    x_val = x_vals[i,j]
                    w_val = w_vals[j,k]
                    # 更新约束
                    self.sub_constraints[idx].lhs = self.alpha[i,j] + x_val * self.beta[j,k]
                    self.sub_constraints[idx].rhs = x_val * w_val
                    idx += 1
        # 更新目标函数
        obj_expr = self.sub_model.sum(self.alpha[i,j] * x_vals[i,j] for i,j in x_vals) + \
                   self.sub_model.sum(self.beta[j,k] * w_vals[j,k] for j,k in w_vals)
        self.sub_model.set_objective('max', obj_expr)

    def __call__(self, context):
        # 仅在MIP节点松弛解可用时触发
        if context.in_relaxation():
            print(f"回调触发于节点 {context.get_node_id()}")
            # 获取当前节点的变量值
            x_vals = {(i,j): context.get_values(self.x_vars[i,j]) for i in self.client_range for j in self.deposit_range}
            w_vals = {(j,k): context.get_values(self.w_vars[j,k]) for j in self.deposit_range for k in self.plant_range}
            
            # 更新并求解子问题
            self._update_subproblem(x_vals, w_vals)
            sub_sol = self.sub_model.solve()
            if not sub_sol:
                return
            
            sub_obj = sub_sol.objective_value
            # 计算主模型对应表达式的当前解值
            main_expr_val = sum(self.tc_dc[j][i] * self.d[i] * x_vals[i,j] for i,j in x_vals) + \
                           sum(self.tc_pd[j][k] * w_vals[j,k] for j,k in w_vals)
            
            # 判断是否需要添加割平面
            if sub_obj > main_expr_val + self.eps:
                # 构造割平面约束(使用主模型变量)
                cut_expr = self.main_model.sum(
                    sub_sol.get_value(self.alpha[i,j]) * self.x_vars[i,j] for i,j in x_vals
                ) + self.main_model.sum(
                    sub_sol.get_value(self.beta[j,k]) * self.w_vars[j,k] for j,k in w_vals
                ) <= sub_obj
                self.register_constraint(cut_expr)
                self.nb_cuts += 1
                print(f"已添加第 {self.nb_cuts} 个割平面")

注册回调的修改

在主模型build_location_model函数中,注册回调时传入必要参数:

# 注册回调
cut_cb = m.register_callback(CustomCutCallback, m, x, w, client_range, deposit_range, plant_range, transport_cost_deposit_client, transport_cost_plant_deposit, d)

备注:内容来源于stack exchange,提问作者Javier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 20:15:27