含二进制变量的约束导致PySCIPOpt求解器异常问题求助
看起来你遇到的问题主要来自非线性约束的处理和大M值的数值稳定性两个方面,我们一步步拆解解决:
问题根源分析
你的三个约束中,约束(2)和(3)都包含二进制变量INDICATOR与连续变量(sum(a)/sum(b))的乘积,属于二次约束。虽然SCIP支持非线性MIP,但默认配置下可能对这类约束的处理不够友好,尤其是当三个约束组合时,会触发求解器的数值异常或兼容性问题。此外,你使用的BIG_NUM=1e15过大,容易导致求解器的精度丢失,进而出现约束违反或无解的反常情况。
另外,从约束逻辑来看,你想实现的应该是:
- 当
INDICATOR=1(净卖出状态):买入总金额sum(a)受换手率限制,卖出总金额sum(b)无限制,且买入≤卖出; - 当
INDICATOR=0(净买入状态):卖出总金额sum(b)受换手率限制,买入总金额sum(a)无限制,且买入可以大于卖出。
但当前的约束(1)在INDICATOR=0时几乎没有限制(sum(a)-sum(b) ≤ 1e15),这也可能导致其他未展示的约束被违反。
具体解决方案
1. 将非线性约束线性化
二进制变量乘连续变量的约束可以转化为等价的线性约束,完全避免非线性问题,这是MIP中处理这类逻辑的标准做法:
替换约束(2)
原约束:INDICATOR * sum(a) ≤ turnover_max * initial_holdings
线性化后:
sum_a = quicksum(a[i] for i in range(num_bonds)) initial_holdings = quicksum(p_bid[i] * x_old[i] for i in range(num_bonds)) # 当INDICATOR=1时,sum_a ≤ turnover_max*initial_holdings;当INDICATOR=0时,sum_a无上限 model.addCons(sum_a <= turnover_max * initial_holdings + BIG_NUM * (1 - INDICATOR))
替换约束(3)
原约束:sum(b) ≤ turnover_max * initial_holdings + INDICATOR * sum(b)
整理后等价于(1-INDICATOR)*sum(b) ≤ turnover_max*initial_holdings,线性化后:
sum_b = quicksum(b[i] for i in range(num_bonds)) # 当INDICATOR=0时,sum_b ≤ turnover_max*initial_holdings;当INDICATOR=1时,sum_b无上限 model.addCons(sum_b <= turnover_max * initial_holdings + BIG_NUM * INDICATOR)
2. 调整大M值的大小
1e15过大,建议根据实际问题的规模设置合理的M值:
- 计算
sum(a)和sum(b)的理论最大值(比如所有债券全部买入/卖出的金额),然后取这个最大值的1.1~2倍作为BIG_NUM,比如:
# 计算理论最大可能的sum(a)或sum(b) max_possible = max(p_ask[i] * x_old[i] for i in range(num_bonds)) * num_bonds BIG_NUM = max_possible * 2
这样既能保证约束的有效性,又能避免数值精度问题。
3. 优化约束(1)的逻辑
原约束(1)在INDICATOR=0时几乎没有限制,如果你想在净买入状态下也有合理的约束(或者明确区分两种状态),可以调整为:
# 当INDICATOR=1时,sum(a) ≤ sum(b)(净卖出);当INDICATOR=0时,sum(b) ≤ sum(a)(净买入) model.addCons(sum_a <= sum_b + BIG_NUM * (1 - INDICATOR)) model.addCons(sum_b <= sum_a + BIG_NUM * INDICATOR)
这样通过两个线性约束明确区分净买入和净卖出两种状态,逻辑更清晰。
4. 启用SCIP的非线性处理参数(可选)
如果你坚持使用原非线性约束,可以尝试设置SCIP的参数来启用非线性MIP支持:
model.setParam("misc/allowstrongdualreds", False) model.setParam("nlp/allowdualreds", False) model.setParam("presolving/maxrounds", 0) # 关闭预求解,避免非线性约束被误处理
不过这种方法不如线性化可靠,优先推荐线性化方案。
完整修改后的代码片段
# Create Variables a, b, x, y = {}, {}, {}, {} # 计算合理的BIG_NUM max_possible_trade = max(p_ask[i] * x_old[i] for i in range(num_bonds)) * num_bonds BIG_NUM = max_possible_trade * 2 for i in range(num_bonds): x[i] = model.addVar(lb=0, ub=None, vtype="C", name=f"x{i}") a[i] = model.addVar(lb=0, ub=None, vtype="C", name=f"a{i}") b[i] = model.addVar(lb=0, ub=None, vtype="C", name=f"b{i}") for t in range(num_periods): y[t] = model.addVar(lb=0, vtype="C", name=f"y{t}") for i in range(num_bonds): # Buy Trades model.addCons(a[i] >= p_ask[i] * (x[i] - x_old[i])) # Sell Trades model.addCons(b[i] >= p_bid[i] * (x_old[i] - x[i])) # 定义求和变量简化代码 sum_a = quicksum(a[i] for i in range(num_bonds)) sum_b = quicksum(b[i] for i in range(num_bonds)) initial_holdings = quicksum(p_bid[i] * x_old[i] for i in range(num_bonds)) # 替换后的约束 INDICATOR = model.addVar(vtype="B", name=f"INDICATOR") # 约束1:区分净买入/净卖出状态 model.addCons(sum_a <= sum_b + BIG_NUM * (1 - INDICATOR)) model.addCons(sum_b <= sum_a + BIG_NUM * INDICATOR) # 约束2:净卖出时买入金额受限 model.addCons(sum_a <= turnover_max * initial_holdings + BIG_NUM * (1 - INDICATOR)) # 约束3:净买入时卖出金额受限 model.addCons(sum_b <= turnover_max * initial_holdings + BIG_NUM * INDICATOR)
这样修改后,所有约束都是线性的,大M值更合理,应该能解决报错和约束违反的问题。
内容的提问来源于stack exchange,提问作者Winsor-Mavis

