Profit Maximization Example
using Optim.jl
This a example on how to use Optim.jl to maximize the profit of a company operating in a defined market (through the OptimalBids
API).
- For this example, we will use a market of type
PowerModelsMarkets
which represents an energy spot market. - Case instance is IEEE 118 Bus Case
- To ilustrate a situation where the company defines the distribution of volume across it's assets in a predefined way (and reduce the decision dimentionality), the only controlable variable to maximize the company profit is a multiplicative factor of all offers.
Include Needed Packages
using OptimalBids
using OptimalBids.PowerModelsMarkets
using HiGHS # Market Clearing Solver
using JuMP: optimizer_with_attributes
using Optim
using Plots # For some evaluation plots at the end
using Plots.PlotMeasures
using Downloads # To download Test Cases
Case Definition
# Read market data from IEEE 118 bus case
case_name = "pglib_opf_case118_ieee.m"
DATA_DIR = mktempdir()
case_file_path = joinpath(DATA_DIR, case_name)
Downloads.download("https://raw.githubusercontent.com/power-grid-lib/pglib-opf/01681386d084d8bd03b429abcd1ee6966f68b9a3/" * case_name, case_file_path)
network_data = PowerModels.parse_file(case_file_path)
# Pretend we are a company constructing a new set of generators in the grid.
# Choose a percentage of the total number of buses to install the new generators:
percentage_buses = 0.09
# We need the keys PowerModels uses to reference the appropriate buses in it's network data dictionary.
# First, find out all available keys:
bus_indexes = collect(keys(network_data["bus"]))
# Then, calculate number of buses that consitute the chose percent (`percentage_buses`):
num_buses = length(bus_indexes)
num_strategic_buses = ceil(Int, percentage_buses * num_buses)
# To avoid any biases let's grab some generators in the middle:
bus_indexes = bus_indexes[21:(21 + num_strategic_buses - 1)]
# Finally, add new generators to the network grid data and collect their reference keys.
generator_indexes = [
add_generator(network_data, parse(Int, bus_idx)) for bus_idx in bus_indexes
]
11-element Vector{String}:
"55"
"56"
"57"
"58"
"59"
"60"
"61"
"62"
"63"
"64"
"65"
OptimalBids API
# Define market
market = build_market(
PowerModelsMarket,
network_data,
generator_indexes,
optimizer_with_attributes(HiGHS.Optimizer, "LogLevel" => 0),
)
# Relative distribution of offers are sometimes predefined and cannot be changed at bidding time.
using Random
rng = MersenneTwister(0)
offer_weights = rand(rng, num_strategic_buses)
offer_weights = offer_weights/ sum(offer_weights)
# However, the decision maker is allowed to increase all bids evenly:
min_total_volume = 0.0
max_total_volume = 65.0
range_mul_factor = min_total_volume:0.1:max_total_volume
bid_range = [offer_weights .* [i] for i in range_mul_factor]
p_curve = profit_curve!(market, bid_range)
# Let's plot and see how the range profit evaluatiuon went:
plt_range = plot(collect(range_mul_factor), p_curve,
label="Range Evaluation",
ylabel="Profit (\$)",
xlabel="Multiplicative Factor",
legend=:outertopright,
left_margin=10mm,
bottom_margin=10mm,
);
plt_comp = deepcopy(plt_range);
Profit Function
# Optim needs a minimization objective function that only receives the decision vector.
function profit_function(total_volume)
global fcalls += 1
return - profit_for_bid!(market, offer_weights .* total_volume[1])
end
profit_function (generic function with 1 method)
NelderMead (0-Order)
# Max Number of Iterations for the solution method (proxy to a time limit at bidding time).
maxiter = 10
global fcalls = 0
# Solve Optim optimization model:
lower = [min_total_volume]
upper = [max_total_volume]
initial_x = [min_total_volume] .+ 0.001 # Initial value cannot be on boundary value
# Limiting fcalls.
function advanced_fcall_control(x)
println(" * Fcalls so far: ", fcalls)
if fcalls >= maxiter
return true
end
println()
false
end
inner_optimizer = NelderMead()
results = Optim.optimize(profit_function, lower, upper, initial_x, Fminbox(inner_optimizer), Optim.Options(f_calls_limit=maxiter, callback = advanced_fcall_control, store_trace=true))
best_solution = results.minimizer
best_profit = -results.minimum
scatter!(plt_comp, [best_solution], [best_profit],
label="NelderMead - OPF Calls:$(fcalls)",
)
LBFGS (1-Order)
# Profit and gradient
using LinearAlgebra
function fg!(F,G,total_volume)
y, Δ = profit_and_gradient_for_bid!(market, offer_weights .* total_volume[1])
global fcalls += 1
isnothing(G) || copyto!(G, dot(Δ, offer_weights))
isnothing(F) || return y
nothing
end
# Max Number of Iterations for the solution method (proxy to a time limit at bidding time).
maxiter = 10
global fcalls = 0
inner_optimizer = LBFGS()
results = Optim.optimize(Optim.only_fg!(fg!), lower, upper, initial_x, Fminbox(inner_optimizer), Optim.Options(f_calls_limit=maxiter, callback = advanced_fcall_control, store_trace=true))
best_solution = results.minimizer
best_profit = -results.minimum
scatter!(plt_comp, [best_solution], [best_profit],
label="LBFGS - OPF Calls:$(fcalls)",
)
Brent (0-Order)
# Max Number of Iterations for the solution method (proxy to a time limit at bidding time).
maxiter = 10
global fcalls = 0
function profit_function(total_volume)
global fcalls += 1
return - profit_for_bid!(market, offer_weights .* total_volume)
end
results = Optim.optimize(profit_function, min_total_volume, max_total_volume, Brent(); callback = advanced_fcall_control, store_trace=true)
best_solution = results.minimizer
best_profit = -results.minimum
scatter!(plt_comp, [best_solution], [best_profit],
label="Brent - OPF Calls:$(fcalls)",
)