Вычисление двойственных решений частично целочисленной программы
Модель
В качестве модели взят пример составления графика нагрузки энергоблоков из раздела Unit commitment. Подробности не важны, за исключением того, что есть два типа непрерывных переменных, g
и w
, представляющих количество вырабатываемой тепловыми и ветровыми электростанциями энергии, и дискретная переменная dispatch
, которая равна 1
, если электростанция i
работает, и 0
в противном случае.
Нас интересует «двойственное» решение ограничения power_balance
, так как оно представляет предельную цену потребляемой электроэнергии.
generators = [
(min = 0.0, max = 1000.0, fixed_cost = 1000.0, variable_cost = 50.0),
(min = 300.0, max = 1000.0, fixed_cost = 0.0, variable_cost = 100.0),
]
N = length(generators)
model = Model(HiGHS.Optimizer)
set_silent(model)
@variables(model, begin
generators[i].min <= g[i = 1:N] <= generators[i].max
0 <= w <= 200
dispatch[i = 1:N], Bin
end)
@constraints(model, begin
power_balance, sum(g[i] for i in 1:N) + w == 1500
[i = 1:N], g[i] <= generators[i].max * dispatch[i]
[i = 1:N], g[i] >= generators[i].min * dispatch[i]
end)
@objective(
model,
Min,
sum(
generators[i].fixed_cost * dispatch[i] +
generators[i].variable_cost * g[i] for i in 1:N
)
)
print(model)
Min 1000 dispatch[1] + 50 g[1] + 100 g[2]
Subject to
power_balance : g[1] + g[2] + w = 1500
g[1] ≥ 0
g[2] - 300 dispatch[2] ≥ 0
g[1] - 1000 dispatch[1] ≤ 0
g[2] - 1000 dispatch[2] ≤ 0
g[1] ≥ 0
g[2] ≥ 300
w ≥ 0
g[1] ≤ 1000
g[2] ≤ 1000
w ≤ 200
dispatch[1] binary
dispatch[2] binary
Фиксация переменных вручную
Если оптимизировать эту модель, мы получим значение NO_SOLUTION
метода dual_status
:
optimize!(model)
@assert is_solved_and_feasible(model)
dual_status(model)
NO_SOLUTION::ResultStatusCode = 0
Причина в том, что HiGHS не может вычислять двойственные решения частично целочисленной программы. Эту проблему можно обойти, зафиксировав целочисленные переменные в точке оптимального решения, ослабив целочисленность и повторно решив задачу как линейную программу.
discrete_values = value.(dispatch)
fix.(dispatch, discrete_values; force = true)
unset_binary.(dispatch)
print(model)
Min 1000 dispatch[1] + 50 g[1] + 100 g[2]
Subject to
power_balance : g[1] + g[2] + w = 1500
g[1] ≥ 0
g[2] - 300 dispatch[2] ≥ 0
g[1] - 1000 dispatch[1] ≤ 0
g[2] - 1000 dispatch[2] ≤ 0
dispatch[1] = 1
dispatch[2] = 1
g[1] ≥ 0
g[2] ≥ 300
w ≥ 0
g[1] ≤ 1000
g[2] ≤ 1000
w ≤ 200
Если теперь решить задачу заново, мы получим FEASIBLE_POINT
для двойственного решения:
optimize!(model)
@assert is_solved_and_feasible(model)
dual_status(model)
FEASIBLE_POINT::ResultStatusCode = 1
и предельную цену электроэнергии 100 долл./МВт·ч:
dual(power_balance)
100.0
Чтобы вернуться к частично целочисленной линейной программе, нужно вызвать unfix
и set_binary
:
unfix.(dispatch)
set_binary.(dispatch)
print(model)
Min 1000 dispatch[1] + 50 g[1] + 100 g[2]
Subject to
power_balance : g[1] + g[2] + w = 1500
g[1] ≥ 0
g[2] - 300 dispatch[2] ≥ 0
g[1] - 1000 dispatch[1] ≤ 0
g[2] - 1000 dispatch[2] ≤ 0
g[1] ≥ 0
g[2] ≥ 300
w ≥ 0
g[1] ≤ 1000
g[2] ≤ 1000
w ≤ 200
dispatch[1] binary
dispatch[2] binary
Использование fix_discrete_variables
Выбор переменных вручную для ослабления и фиксации подходит для нашего небольшого примера, но затруднительнее в задачах с большим количеством двоичных и целочисленных переменных. Для автоматизации процесса, который мы только что выполнили вручную, JuMP предоставляет функцию fix_discrete_variables
:
optimize!(model)
@assert is_solved_and_feasible(model)
dual_status(model)
NO_SOLUTION::ResultStatusCode = 0
undo = fix_discrete_variables(model);
Здесь undo
— это функция, которая при вызове без аргументов возвращает модель к исходной частично целочисленной формулировке.
После вызова |
print(model)
Min 1000 dispatch[1] + 50 g[1] + 100 g[2]
Subject to
power_balance : g[1] + g[2] + w = 1500
g[1] ≥ 0
g[2] - 300 dispatch[2] ≥ 0
g[1] - 1000 dispatch[1] ≤ 0
g[2] - 1000 dispatch[2] ≤ 0
dispatch[1] = 1
dispatch[2] = 1
g[1] ≥ 0
g[2] ≥ 300
w ≥ 0
g[1] ≤ 1000
g[2] ≤ 1000
w ≤ 200
optimize!(model)
@assert is_solved_and_feasible(model)
dual_status(model)
FEASIBLE_POINT::ResultStatusCode = 1
dual(power_balance)
100.0
Наконец, вызовем undo
, чтобы отменить переформулировку:
undo()
print(model)
Min 1000 dispatch[1] + 50 g[1] + 100 g[2]
Subject to
power_balance : g[1] + g[2] + w = 1500
g[1] ≥ 0
g[2] - 300 dispatch[2] ≥ 0
g[1] - 1000 dispatch[1] ≤ 0
g[2] - 1000 dispatch[2] ≤ 0
g[1] ≥ 0
g[2] ≥ 300
w ≥ 0
g[1] ≤ 1000
g[2] ≤ 1000
w ≤ 200
dispatch[1] binary
dispatch[2] binary