Энергетические системы
Это руководство было изначально предоставлено Юрием Дворкиным (Yury Dvorkin) и Майлзом Любиным (Miles Lubin).
В этом руководстве демонстрируется формулирование базовых моделей проектирования энергосистем в JuMP.
Мы будем рассматривать базовые модели « экономичного распределения нагрузки» и «составления графика нагрузки энергоблоков», не принимая во внимание ограничения передачи.
В этом руководстве используются следующие пакеты:
using JuMP
import DataFrames
import HiGHS
import Plots
import StatsPlots
Экономичное распределение нагрузки
Экономичное распределение нагрузки (ED) — это задача оптимизации, цель которой — минимизировать затраты на удовлетворение спроса на электроэнергию с учетом эксплуатационных ограничений активов энергосистемы. В простейшей модификации ED представляет собой задачу линейного программирования, решаемую для прогноза совокупной нагрузки и силы ветра и для одного бесконечно малого момента времени.
Математически задачу ED можно записать следующим образом:
где и — это инкрементные расходы (долл./МВт·ч) и выходная мощность (МВт) generator, respectively, and and are the incremental cost ($/МВт·ч) и подача ветровой энергии (МВт) соответственно.
Действуют следующие ограничения:
-
Минимальный ( ) и максимальный ( ) пределы выходной мощности генераторов:
-
Ограничение на подачу ветровой мощности: где и — это соответственно подача ветровой мощности и прогноз ветровой мощности.
-
Ограничение баланса мощности: где — это прогнозируемое потребление.
Дополнительные сведения о моделях ED можно найти в работе A. J. Wood, B. F. Wollenberg, and G. B. Sheblé, Power Generation, Operation and Control, Wiley, 2013.
Определим некоторые входные данные тестовой системы.
Определим несколько тепловых генераторов:
function ThermalGenerator(
min::Float64,
max::Float64,
fixed_cost::Float64,
variable_cost::Float64,
)
return (
min = min,
max = max,
fixed_cost = fixed_cost,
variable_cost = variable_cost,
)
end
generators = [
ThermalGenerator(0.0, 1000.0, 1000.0, 50.0),
ThermalGenerator(300.0, 1000.0, 0.0, 100.0),
]
2-element Vector{@NamedTuple{min::Float64, max::Float64, fixed_cost::Float64, variable_cost::Float64}}:
(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)
Ветрогенератор:
WindGenerator(variable_cost::Float64) = (variable_cost = variable_cost,)
wind_generator = WindGenerator(50.0)
(variable_cost = 50.0,)
И сценарий:
function Scenario(demand::Float64, wind::Float64)
return (demand = demand, wind = wind)
end
scenario = Scenario(1500.0, 200.0)
(demand = 1500.0, wind = 200.0)
Создадим функцию solve_economic_dispatch
, которая решает задачу экономичного распределения нагрузки для заданного набора входных параметров.
function solve_economic_dispatch(generators::Vector, wind, scenario)
# Определяем модель экономичного распределения нагрузки (ED)
model = Model(HiGHS.Optimizer)
set_silent(model)
# Определяем переменные решения
# выходная мощность генераторов
N = length(generators)
@variable(model, generators[i].min <= g[i = 1:N] <= generators[i].max)
# подача ветровой мощности
@variable(model, 0 <= w <= scenario.wind)
# Определяем целевую функцию
@objective(
model,
Min,
sum(generators[i].variable_cost * g[i] for i in 1:N) +
wind.variable_cost * w,
)
# Определяем ограничение баланса мощности
@constraint(model, sum(g[i] for i in 1:N) + w == scenario.demand)
# Оператор решения
optimize!(model)
@assert is_solved_and_feasible(model)
# Возвращаем оптимальное значение целевой функции и ее минимизаторы
return (
g = value.(g),
w = value(w),
wind_spill = scenario.wind - value(w),
total_cost = objective_value(model),
)
end
solve_economic_dispatch (generic function with 1 method)
Решим задачу экономичного распределения нагрузки:
solution = solve_economic_dispatch(generators, wind_generator, scenario);
println("Dispatch of Generators: ", solution.g, " MW")
println("Dispatch of Wind: ", solution.w, " MW")
println("Wind spillage: ", solution.wind_spill, " MW")
println("Total cost: \$", solution.total_cost)
Dispatch of Generators: [1000.0, 300.0] MW
Dispatch of Wind: 200.0 MW
Wind spillage: 0.0 MW
Total cost: $90000.0
Экономичное распределение нагрузки с регулируемыми инкрементными расходами
В следующем упражнении мы скорректируем инкрементные расходы генератора G1 и пронаблюдаем, как это повлияет на общие расходы.
function scale_generator_cost(g, scale)
return ThermalGenerator(g.min, g.max, g.fixed_cost, scale * g.variable_cost)
end
start = time()
c_g_scale_df = DataFrames.DataFrame(;
# Коэффициент масштабирования
scale = Float64[],
# Распределение генератора 1 [МВт]
dispatch_G1 = Float64[],
# Распределение генератора 2 [МВт]
dispatch_G2 = Float64[],
# Распределение ветрогенератора [МВт]
dispatch_wind = Float64[],
# Потери ветровой мощности [МВт]
spillage_wind = Float64[],
# Общие расходы [$]
total_cost = Float64[],
)
for c_g1_scale in 0.5:0.1:3.0
# Инкрементные расходы первого генератора обновляются в каждой итерации.
new_generators = scale_generator_cost.(generators, [c_g1_scale, 1.0])
# Решаем задачу экономичного распределения нагрузки с обновленными инкрементными расходами
sol = solve_economic_dispatch(new_generators, wind_generator, scenario)
push!(
c_g_scale_df,
(c_g1_scale, sol.g[1], sol.g[2], sol.w, sol.wind_spill, sol.total_cost),
)
end
print(string("elapsed time: ", time() - start, " seconds"))
elapsed time: 0.30464911460876465 seconds
c_g_scale_df
Row | scale | dispatch_G1 | dispatch_G2 | dispatch_wind | spillage_wind | total_cost |
---|---|---|---|---|---|---|
Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 0.5 | 1000.0 | 300.0 | 200.0 | 0.0 | 65000.0 |
2 | 0.6 | 1000.0 | 300.0 | 200.0 | 0.0 | 70000.0 |
3 | 0.7 | 1000.0 | 300.0 | 200.0 | 0.0 | 75000.0 |
4 | 0.8 | 1000.0 | 300.0 | 200.0 | 0.0 | 80000.0 |
5 | 0.9 | 1000.0 | 300.0 | 200.0 | 0.0 | 85000.0 |
6 | 1.0 | 1000.0 | 300.0 | 200.0 | 0.0 | 90000.0 |
7 | 1.1 | 1000.0 | 300.0 | 200.0 | 0.0 | 95000.0 |
8 | 1.2 | 1000.0 | 300.0 | 200.0 | 0.0 | 100000.0 |
9 | 1.3 | 1000.0 | 300.0 | 200.0 | 0.0 | 105000.0 |
10 | 1.4 | 1000.0 | 300.0 | 200.0 | 0.0 | 110000.0 |
11 | 1.5 | 1000.0 | 300.0 | 200.0 | 0.0 | 115000.0 |
12 | 1.6 | 1000.0 | 300.0 | 200.0 | 0.0 | 120000.0 |
13 | 1.7 | 1000.0 | 300.0 | 200.0 | 0.0 | 125000.0 |
14 | 1.8 | 1000.0 | 300.0 | 200.0 | 0.0 | 130000.0 |
15 | 1.9 | 1000.0 | 300.0 | 200.0 | 0.0 | 135000.0 |
16 | 2.0 | 300.0 | 1000.0 | 200.0 | 0.0 | 140000.0 |
17 | 2.1 | 300.0 | 1000.0 | 200.0 | 0.0 | 141500.0 |
18 | 2.2 | 300.0 | 1000.0 | 200.0 | 0.0 | 143000.0 |
19 | 2.3 | 300.0 | 1000.0 | 200.0 | 0.0 | 144500.0 |
20 | 2.4 | 300.0 | 1000.0 | 200.0 | 0.0 | 146000.0 |
21 | 2.5 | 300.0 | 1000.0 | 200.0 | 0.0 | 147500.0 |
22 | 2.6 | 300.0 | 1000.0 | 200.0 | 0.0 | 149000.0 |
23 | 2.7 | 300.0 | 1000.0 | 200.0 | 0.0 | 150500.0 |
24 | 2.8 | 300.0 | 1000.0 | 200.0 | 0.0 | 152000.0 |
25 | 2.9 | 300.0 | 1000.0 | 200.0 | 0.0 | 153500.0 |
26 | 3.0 | 300.0 | 1000.0 | 200.0 | 0.0 | 155000.0 |
Изменяем модель JuMP на месте
Обратите внимание, что в предыдущем упражнении модель оптимизации полностью перестраивалась в каждой итерации внутреннего цикла, что создавало дополнительную вычислительную нагрузку. Эту нагрузку можно снизить, если вместо перестройки всей модели будут изменяться только ограничения или целевая функция, как показано в примере ниже.
Сравните время вычисления для приведенных выше и ниже моделей.
function solve_economic_dispatch_inplace(
generators::Vector,
wind,
scenario,
scale::AbstractVector{Float64},
)
obj_out = Float64[]
w_out = Float64[]
g1_out = Float64[]
g2_out = Float64[]
# Эта функция работает только для двух генераторов
@assert length(generators) == 2
model = Model(HiGHS.Optimizer)
set_silent(model)
N = length(generators)
@variable(model, generators[i].min <= g[i = 1:N] <= generators[i].max)
@variable(model, 0 <= w <= scenario.wind)
@objective(
model,
Min,
sum(generators[i].variable_cost * g[i] for i in 1:N) +
wind.variable_cost * w,
)
@constraint(model, sum(g[i] for i in 1:N) + w == scenario.demand)
for c_g1_scale in scale
@objective(
model,
Min,
c_g1_scale * generators[1].variable_cost * g[1] +
generators[2].variable_cost * g[2] +
wind.variable_cost * w,
)
optimize!(model)
@assert is_solved_and_feasible(model)
push!(obj_out, objective_value(model))
push!(w_out, value(w))
push!(g1_out, value(g[1]))
push!(g2_out, value(g[2]))
end
df = DataFrames.DataFrame(;
scale = scale,
dispatch_G1 = g1_out,
dispatch_G2 = g2_out,
dispatch_wind = w_out,
spillage_wind = scenario.wind .- w_out,
total_cost = obj_out,
)
return df
end
start = time()
inplace_df = solve_economic_dispatch_inplace(
generators,
wind_generator,
scenario,
0.5:0.1:3.0,
)
print(string("elapsed time: ", time() - start, " seconds"))
elapsed time: 0.22562503814697266 seconds
Для небольших моделей корректировка определенных ограничений или целевой функции иногда повышает, а иногда снижает скорость выполнения по сравнению с перестроением всей модели. Однако по мере увеличения размера задачи обновление модели на месте обычно происходит быстрее.
inplace_df
Row | scale | dispatch_G1 | dispatch_G2 | dispatch_wind | spillage_wind | total_cost |
---|---|---|---|---|---|---|
Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 0.5 | 1000.0 | 300.0 | 200.0 | 0.0 | 65000.0 |
2 | 0.6 | 1000.0 | 300.0 | 200.0 | 0.0 | 70000.0 |
3 | 0.7 | 1000.0 | 300.0 | 200.0 | 0.0 | 75000.0 |
4 | 0.8 | 1000.0 | 300.0 | 200.0 | 0.0 | 80000.0 |
5 | 0.9 | 1000.0 | 300.0 | 200.0 | 0.0 | 85000.0 |
6 | 1.0 | 1000.0 | 300.0 | 200.0 | 0.0 | 90000.0 |
7 | 1.1 | 1000.0 | 300.0 | 200.0 | 0.0 | 95000.0 |
8 | 1.2 | 1000.0 | 300.0 | 200.0 | 0.0 | 100000.0 |
9 | 1.3 | 1000.0 | 300.0 | 200.0 | 0.0 | 105000.0 |
10 | 1.4 | 1000.0 | 300.0 | 200.0 | 0.0 | 110000.0 |
11 | 1.5 | 1000.0 | 300.0 | 200.0 | 0.0 | 115000.0 |
12 | 1.6 | 1000.0 | 300.0 | 200.0 | 0.0 | 120000.0 |
13 | 1.7 | 1000.0 | 300.0 | 200.0 | 0.0 | 125000.0 |
14 | 1.8 | 1000.0 | 300.0 | 200.0 | 0.0 | 130000.0 |
15 | 1.9 | 1000.0 | 300.0 | 200.0 | 0.0 | 135000.0 |
16 | 2.0 | 1000.0 | 300.0 | 200.0 | 0.0 | 140000.0 |
17 | 2.1 | 300.0 | 1000.0 | 200.0 | 0.0 | 141500.0 |
18 | 2.2 | 300.0 | 1000.0 | 200.0 | 0.0 | 143000.0 |
19 | 2.3 | 300.0 | 1000.0 | 200.0 | 0.0 | 144500.0 |
20 | 2.4 | 300.0 | 1000.0 | 200.0 | 0.0 | 146000.0 |
21 | 2.5 | 300.0 | 1000.0 | 200.0 | 0.0 | 147500.0 |
22 | 2.6 | 300.0 | 1000.0 | 200.0 | 0.0 | 149000.0 |
23 | 2.7 | 300.0 | 1000.0 | 200.0 | 0.0 | 150500.0 |
24 | 2.8 | 300.0 | 1000.0 | 200.0 | 0.0 | 152000.0 |
25 | 2.9 | 300.0 | 1000.0 | 200.0 | 0.0 | 153500.0 |
26 | 3.0 | 300.0 | 1000.0 | 200.0 | 0.0 | 155000.0 |
Неэффективное использование ветрогенераторов
Задача экономичного распределения нагрузки не предполагает принятия решений о пуске и останове энергоблоков, поэтому предполагается, что все генераторы должны диспетчеризоваться по крайней мере на минимальном уровне выходной мощности. Такой подход неэффективен с точки зрения затрат и может приводить к абсурдным решениям. Например, если , то подача ветровой мощности должна быть равна нулю, то есть вся доступная ветровая генерация теряется для выполнения ограничений на минимальную выходную мощность генераторов.
В следующем примере мы корректируем общее потребление и наблюдаем, как это влияет на потери ветровой мощности.
demand_scale_df = DataFrames.DataFrame(;
demand = Float64[],
dispatch_G1 = Float64[],
dispatch_G2 = Float64[],
dispatch_wind = Float64[],
spillage_wind = Float64[],
total_cost = Float64[],
)
function scale_demand(scenario, scale)
return Scenario(scale * scenario.demand, scenario.wind)
end
for demand_scale in 0.2:0.1:1.4
new_scenario = scale_demand(scenario, demand_scale)
sol = solve_economic_dispatch(generators, wind_generator, new_scenario)
push!(
demand_scale_df,
(
new_scenario.demand,
sol.g[1],
sol.g[2],
sol.w,
sol.wind_spill,
sol.total_cost,
),
)
end
demand_scale_df
Row | demand | dispatch_G1 | dispatch_G2 | dispatch_wind | spillage_wind | total_cost |
---|---|---|---|---|---|---|
Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 300.0 | 0.0 | 300.0 | 0.0 | 200.0 | 30000.0 |
2 | 450.0 | 150.0 | 300.0 | 0.0 | 200.0 | 37500.0 |
3 | 600.0 | 300.0 | 300.0 | 0.0 | 200.0 | 45000.0 |
4 | 750.0 | 450.0 | 300.0 | 0.0 | 200.0 | 52500.0 |
5 | 900.0 | 600.0 | 300.0 | 0.0 | 200.0 | 60000.0 |
6 | 1050.0 | 750.0 | 300.0 | 0.0 | 200.0 | 67500.0 |
7 | 1200.0 | 900.0 | 300.0 | 0.0 | 200.0 | 75000.0 |
8 | 1350.0 | 850.0 | 300.0 | 200.0 | 0.0 | 82500.0 |
9 | 1500.0 | 1000.0 | 300.0 | 200.0 | 0.0 | 90000.0 |
10 | 1650.0 | 1000.0 | 450.0 | 200.0 | 0.0 | 105000.0 |
11 | 1800.0 | 1000.0 | 600.0 | 200.0 | 0.0 | 120000.0 |
12 | 1950.0 | 1000.0 | 750.0 | 200.0 | 0.0 | 135000.0 |
13 | 2100.0 | 1000.0 | 900.0 | 200.0 | 0.0 | 150000.0 |
dispatch_plot = StatsPlots.@df(
demand_scale_df,
Plots.plot(
:demand,
[:dispatch_G1, :dispatch_G2],
labels = ["G1" "G2"],
title = "Thermal Dispatch",
legend = :bottomright,
linewidth = 3,
xlabel = "Demand",
ylabel = "Dispatch [MW]",
),
)
wind_plot = StatsPlots.@df(
demand_scale_df,
Plots.plot(
:demand,
[:dispatch_wind, :spillage_wind],
labels = ["Dispatch" "Spillage"],
title = "Wind",
legend = :bottomright,
linewidth = 3,
xlabel = "Demand [MW]",
ylabel = "Energy [MW]",
),
)
Plots.plot(dispatch_plot, wind_plot)
Этот недостаток можно преодолеть, введя двоичные переменные решения по состоянию включения генераторов. Такая модель называется моделью составления графика нагрузки энергоблоков и рассматривается далее в этом руководстве.
За дополнительной информацией о взаимосвязи между ветровой генерацией и ограничениями на минимальную выходную мощность генераторов мы отсылаем заинтересованных читателей к статье R. Baldick, Wind and energy markets: a case study of Texas, IEEE Systems Journal, vol. 6, pp. 27—34, 2012.
Составление графика нагрузки энергоблоков
Модель составления графика нагрузки энергоблоков (UC) можно получить из модели ED путем введения двоичной переменной, связанной с каждым генератором. Эта двоичная переменная может принимать два значения: если она равна 1, генератор синхронизирован и поэтому может быть запущен; в противном случае, то есть если двоичная переменная равна 0, генератор не синхронизирован и его выходная мощность установлена на 0.
Чтобы получить математическую формулировку модели UC, мы изменим ограничения модели ED следующим образом:
где $u_{i} \in {0,1}.$ В этом ограничении, если $u_{i} = 0$, то $g_{i} = 0$. С другой стороны, если $u_{i} = 1$, то $g^{min}_{i} \leq g_{i} \leq g^{max}_{i}$.
За дополнительной информацией о задаче UC мы отсылаем заинтересованных читателей к статье G. Morales-Espana, J. M. Latorre, and A. Ramos, Tight and Compact MILP Formulation for the Thermal Unit Commitment Problem, IEEE Transactions on Power Systems, vol. 28, pp. 4897—4908, 2013.
В следующем примере описанная выше модель ED преобразовывается в модель UC.
function solve_unit_commitment(generators::Vector, wind, scenario)
model = Model(HiGHS.Optimizer)
set_silent(model)
N = length(generators)
@variable(model, 0 <= g[i = 1:N] <= generators[i].max)
@variable(model, 0 <= w <= scenario.wind)
@constraint(model, sum(g[i] for i in 1:N) + w == scenario.demand)
# !!! Нововведение: добавляем двоичные переменные включения-выключения для каждого генератора
@variable(model, u[i = 1:N], Bin)
@constraint(model, [i = 1:N], g[i] <= generators[i].max * u[i])
@constraint(model, [i = 1:N], g[i] >= generators[i].min * u[i])
@objective(
model,
Min,
sum(generators[i].variable_cost * g[i] for i in 1:N) +
wind.variable_cost * w +
# !!! новое
sum(generators[i].fixed_cost * u[i] for i in 1:N)
)
optimize!(model)
status = termination_status(model)
if status != OPTIMAL
return (status = status,)
end
@assert primal_status(model) == FEASIBLE_POINT
return (
status = status,
g = value.(g),
w = value(w),
wind_spill = scenario.wind - value(w),
u = value.(u),
total_cost = objective_value(model),
)
end
solve_unit_commitment (generic function with 1 method)
Решаем задачу составления графика нагрузки энергоблоков
solution = solve_unit_commitment(generators, wind_generator, scenario)
println("Dispatch of Generators: ", solution.g, " MW")
println("Commitments of Generators: ", solution.u)
println("Dispatch of Wind: ", solution.w, " MW")
println("Wind spillage: ", solution.wind_spill, " MW")
println("Total cost: \$", solution.total_cost)
Dispatch of Generators: [1000.0, 300.0] MW
Commitments of Generators: [1.0, 1.0]
Dispatch of Wind: 200.0 MW
Wind spillage: 0.0 MW
Total cost: $91000.0
Составление графика нагрузки энергоблоков как функция от потребления
Реализовав модель составления графика нагрузки энергоблоков, теперь мы можем оценить взаимосвязь между минимальными ограничениями на минимальную выходную мощность генераторов и ветрогенерацией.
uc_df = DataFrames.DataFrame(;
demand = Float64[],
commitment_G1 = Float64[],
commitment_G2 = Float64[],
dispatch_G1 = Float64[],
dispatch_G2 = Float64[],
dispatch_wind = Float64[],
spillage_wind = Float64[],
total_cost = Float64[],
)
for demand_scale in 0.2:0.1:1.4
new_scenario = scale_demand(scenario, demand_scale)
sol = solve_unit_commitment(generators, wind_generator, new_scenario)
if sol.status == OPTIMAL
push!(
uc_df,
(
new_scenario.demand,
sol.u[1],
sol.u[2],
sol.g[1],
sol.g[2],
sol.w,
sol.wind_spill,
sol.total_cost,
),
)
end
println("Status: stem:[(sol.status) for demand_scale = ](demand_scale)")
end
Status: OPTIMAL for demand_scale = 0.2
Status: OPTIMAL for demand_scale = 0.3
Status: OPTIMAL for demand_scale = 0.4
Status: OPTIMAL for demand_scale = 0.5
Status: OPTIMAL for demand_scale = 0.6
Status: OPTIMAL for demand_scale = 0.7
Status: OPTIMAL for demand_scale = 0.8
Status: OPTIMAL for demand_scale = 0.9
Status: OPTIMAL for demand_scale = 1.0
Status: OPTIMAL for demand_scale = 1.1
Status: OPTIMAL for demand_scale = 1.2
Status: OPTIMAL for demand_scale = 1.3
Status: OPTIMAL for demand_scale = 1.4
uc_df
Row | demand | commitment_G1 | commitment_G2 | dispatch_G1 | dispatch_G2 | dispatch_wind | spillage_wind | total_cost |
---|---|---|---|---|---|---|---|---|
Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
1 | 300.0 | 1.0 | 0.0 | 100.0 | 0.0 | 200.0 | 0.0 | 16000.0 |
2 | 450.0 | 1.0 | 0.0 | 250.0 | 0.0 | 200.0 | 0.0 | 23500.0 |
3 | 600.0 | 1.0 | 0.0 | 400.0 | 0.0 | 200.0 | 0.0 | 31000.0 |
4 | 750.0 | 1.0 | 0.0 | 550.0 | 0.0 | 200.0 | 0.0 | 38500.0 |
5 | 900.0 | 1.0 | 0.0 | 700.0 | 0.0 | 200.0 | 0.0 | 46000.0 |
6 | 1050.0 | 1.0 | 0.0 | 850.0 | 0.0 | 200.0 | 0.0 | 53500.0 |
7 | 1200.0 | 1.0 | 0.0 | 1000.0 | 0.0 | 200.0 | 0.0 | 61000.0 |
8 | 1350.0 | 1.0 | 1.0 | 850.0 | 300.0 | 200.0 | 0.0 | 83500.0 |
9 | 1500.0 | 1.0 | 1.0 | 1000.0 | 300.0 | 200.0 | 0.0 | 91000.0 |
10 | 1650.0 | 1.0 | 1.0 | 1000.0 | 450.0 | 200.0 | 0.0 | 106000.0 |
11 | 1800.0 | 1.0 | 1.0 | 1000.0 | 600.0 | 200.0 | 0.0 | 121000.0 |
12 | 1950.0 | 1.0 | 1.0 | 1000.0 | 750.0 | 200.0 | 0.0 | 136000.0 |
13 | 2100.0 | 1.0 | 1.0 | 1000.0 | 900.0 | 200.0 | 0.0 | 151000.0 |
commitment_plot = StatsPlots.@df(
uc_df,
Plots.plot(
:demand,
[:commitment_G1, :commitment_G2],
labels = ["G1" "G2"],
title = "Commitment",
legend = :bottomright,
linewidth = 3,
xlabel = "Demand [MW]",
ylabel = "Commitment decision {0, 1}",
),
)
dispatch_plot = StatsPlots.@df(
uc_df,
Plots.plot(
:demand,
[:dispatch_G1, :dispatch_G2, :dispatch_wind],
labels = ["G1" "G2" "Wind"],
title = "Dispatch [MW]",
legend = :bottomright,
linewidth = 3,
xlabel = "Demand",
ylabel = "Dispatch [MW]",
),
)
Plots.plot(commitment_plot, dispatch_plot)
Нелинейное экономичное распределение нагрузки
В качестве последнего примера мы изменим задачу экономичного распределения нагрузки двумя способами:
-
Функция тепловых затрат определяется пользователем
-
Выход ветровой энергии равен всего лишь квадратному корню из распределения
import Ipopt
"""
thermal_cost_function(g)
A user-defined thermal cost function in pure-Julia! You can include
nonlinearities, and even things like control flow.
[WARNING]
====
It's still up to you to make sure that the function has a meaningful
====
derivative.
"""
function thermal_cost_function(g)
if g <= 500
return g
else
return g + 1e-2 * (g - 500)^2
end
end
function solve_nonlinear_economic_dispatch(
generators::Vector,
wind,
scenario;
silent::Bool = false,
)
model = Model(Ipopt.Optimizer)
if silent
set_silent(model)
end
@operator(model, op_tcf, 1, thermal_cost_function)
N = length(generators)
@variable(model, generators[i].min <= g[i = 1:N] <= generators[i].max)
@variable(model, 0 <= w <= scenario.wind)
@objective(
model,
Min,
sum(generators[i].variable_cost * op_tcf(g[i]) for i in 1:N) +
wind.variable_cost * w,
)
@constraint(model, sum(g[i] for i in 1:N) + sqrt(w) == scenario.demand)
optimize!(model)
@assert is_solved_and_feasible(model)
return (
g = value.(g),
w = value(w),
wind_spill = scenario.wind - value(w),
total_cost = objective_value(model),
)
end
solution =
solve_nonlinear_economic_dispatch(generators, wind_generator, scenario)
(g = [847.3509933774712, 648.6754966887423], w = 15.78878119389903, wind_spill = 184.21121880610096, total_cost = 190455.298013245)
Теперь давайте посмотрим, как распределяется ветровая энергия в зависимости от расходов:
wind_cost = 0.0:1:100
wind_dispatch = Float64[]
for c in wind_cost
sol = solve_nonlinear_economic_dispatch(
generators,
WindGenerator(c),
scenario;
silent = true,
)
push!(wind_dispatch, sol.w)
end
Plots.plot(
wind_cost,
wind_dispatch;
xlabel = "Cost",
ylabel = "Dispatch [MW]",
label = false,
)