Skip to content

Commit

Permalink
Allow writing of multistage stats during optimization
Browse files Browse the repository at this point in the history
  • Loading branch information
lbonaldo committed Apr 23, 2024
1 parent 8c3e828 commit e45ed43
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 55 deletions.
12 changes: 6 additions & 6 deletions src/case_runners/case_runner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,7 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict)
### Solve model
println("Solving Model")

# Step 3) Run DDP Algorithm
## Solve Model
model_dict, mystats_d, inputs_dict = run_ddp(model_dict, mysetup, inputs_dict)

# Step 4) Write final outputs from each stage

# Prepare folder for results
outpath = get_default_output_folder(case)

if mysetup["OverwriteResults"] == 1
Expand All @@ -160,6 +155,11 @@ function run_genx_case_multistage!(case::AbstractString, mysetup::Dict)
mkdir(outpath)
end

# Step 3) Run DDP Algorithm
## Solve Model
model_dict, mystats_d, inputs_dict = run_ddp(outpath, model_dict, mysetup, inputs_dict)

# Step 4) Write final outputs from each stage
for p in 1:mysetup["MultiStageSettingsDict"]["NumStages"]
outpath_cur = joinpath(outpath, "Results_p$p")
write_outputs(model_dict[p], outpath_cur, mysetup, inputs_dict[p])
Expand Down
51 changes: 9 additions & 42 deletions src/multi_stage/dual_dynamic_programming.jl
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ returns:
* stats\_d – Dictionary which contains the run time, upper bound, and lower bound of each DDP iteration.
* inputs\_d – Dictionary of inputs for each model stage, generated by the load\_inputs() method, modified by this method.
"""
function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)

function run_ddp(outpath::AbstractString, models_d::Dict, setup::Dict, inputs_d::Dict)
settings_d = setup["MultiStageSettingsDict"]
num_stages = settings_d["NumStages"] # Total number of investment planning stages
EPSILON = settings_d["ConvergenceTolerance"] # Tolerance
Expand All @@ -75,10 +74,13 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)
ic = 0 # Iteration Counter

results_d = Dict() # Dictionary to store the results to return
stats_d = Dict() # Dictionary to store the statistics (total time, upper bound, and lower bound for each iteration)
times_a = [] # Array to store the total time of each iteration
upper_bounds_a = [] # Array to store the upper bound of each iteration
lower_bounds_a = [] # Array to store the lower bound of each iteration
stats_d = Dict() # Dictionary to store the statistics (total time, upper bound, and lower bound for each iteration)
stats_d["TIMES"] = times_a
stats_d["UPPER_BOUNDS"] = upper_bounds_a
stats_d["LOWER_BOUNDS"] = lower_bounds_a

# Step a.i) Initialize cost-to-go function for t = 1:num_stages
for t in 1:num_stages
Expand Down Expand Up @@ -116,10 +118,6 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)
println(string("Lower Bound = ", z_lower))
println("***********")

stats_d["TIMES"] = times_a
stats_d["UPPER_BOUNDS"] = upper_bounds_a
statd_d["LOWER_BOUNDS"] = lower_bounds_a

return models_d, stats_d, inputs_d
end

Expand Down Expand Up @@ -163,10 +161,6 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)
println(string("Upper Bound = ", z_upper))
println(string("Lower Bound = ", z_lower))
println("***********")

stats_d["TIMES"] = times_a
stats_d["UPPER_BOUNDS"] = upper_bounds_a
stats_d["LOWER_BOUNDS"] = lower_bounds_a
return models_d, stats_d, inputs_d
end
###
Expand All @@ -183,6 +177,7 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)
end

append!(upper_bounds_a, z_upper) # Store current iteration upper bound
update_multi_stage_stats_file(outpath, ic, z_upper, z_lower, NaN, new_row=true)

# Step f) Backward pass for t = num_stages:2
for t in num_stages:-1:2
Expand All @@ -202,10 +197,13 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)
# Step g) Recalculate lower bound and go back to c)
z_lower = objective_value(models_d[1])
append!(lower_bounds_a, z_lower) # Store current iteration lower bound
update_multi_stage_stats_file(outpath, ic, z_upper, z_lower, NaN)

# Step h) Store the total time of the current iteration (in seconds)
ddp_iteration_time = time() - ddp_prev_time
append!(times_a, ddp_iteration_time)
update_multi_stage_stats_file(outpath, ic, z_upper, z_lower, ddp_iteration_time)

ddp_prev_time = time()
end

Expand Down Expand Up @@ -239,40 +237,9 @@ function run_ddp(models_d::Dict, setup::Dict, inputs_d::Dict)
end
##### END of final forward pass

stats_d["TIMES"] = times_a
stats_d["UPPER_BOUNDS"] = upper_bounds_a
stats_d["LOWER_BOUNDS"] = lower_bounds_a

return models_d, stats_d, inputs_d
end

@doc raw"""
function write_multi_stage_outputs(stats_d::Dict, outpath::String, settings_d::Dict)
This function calls various methods which write multi-stage modeling outputs as .csv files.
inputs:
* stats\_d – Dictionary which contains the run time, upper bound, and lower bound of each DDP iteration.
* outpath – String which represents the path to the Results directory.
* settings\_d - Dictionary containing settings configured in the GenX settings genx\_settings.yml file as well as the multi-stage settings file multi\_stage\_settings.yml.
"""
function write_multi_stage_outputs(stats_d::Dict, outpath::String, settings_d::Dict, inputs_dict::Dict)

multi_stage_settings_d = settings_d["MultiStageSettingsDict"]

write_multi_stage_capacities_discharge(outpath, multi_stage_settings_d)
write_multi_stage_capacities_charge(outpath, multi_stage_settings_d)
write_multi_stage_capacities_energy(outpath, multi_stage_settings_d)
if settings_d["NetworkExpansion"] == 1
write_multi_stage_network_expansion(outpath, multi_stage_settings_d)
end
write_multi_stage_costs(outpath, multi_stage_settings_d, inputs_dict)
write_multi_stage_stats(outpath, stats_d)
write_multi_stage_settings(outpath, settings_d)

end

@doc raw"""
function fix_initial_investments(EP_prev::Model, EP_cur::Model, start_cap_d::Dict)
Expand Down
30 changes: 30 additions & 0 deletions src/multi_stage/write_multi_stage_outputs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@doc raw"""
write_multi_stage_outputs(stats_d::Dict,
outpath::String,
settings_d::Dict,
inputs_dict::Dict)
This function calls various methods which write multi-stage modeling outputs as .csv files.
# Arguments:
* stats\_d: Dictionary which contains the run time, upper bound, and lower bound of each DDP iteration.
* outpath: String which represents the path to the Results directory.
* settings\_d: Dictionary containing settings configured in the GenX settings `genx_settings.yml` file as well as the multi-stage settings file `multi_stage_settings.yml`.
* inputs\_dict: Dictionary containing the input data for the multi-stage model.
"""
function write_multi_stage_outputs(stats_d::Dict,
outpath::String,
settings_d::Dict,
inputs_dict::Dict)
multi_stage_settings_d = settings_d["MultiStageSettingsDict"]

write_multi_stage_capacities_discharge(outpath, multi_stage_settings_d)
write_multi_stage_capacities_charge(outpath, multi_stage_settings_d)
write_multi_stage_capacities_energy(outpath, multi_stage_settings_d)
if settings_d["NetworkExpansion"] == 1
write_multi_stage_network_expansion(outpath, multi_stage_settings_d)
end
write_multi_stage_costs(outpath, multi_stage_settings_d, inputs_dict)
multi_stage_settings_d["Myopic"] == 0 && write_multi_stage_stats(outpath, stats_d)
write_multi_stage_settings(outpath, settings_d)
end
84 changes: 77 additions & 7 deletions src/multi_stage/write_multi_stage_stats.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
_get_multi_stage_stats_filename() = "stats_multi_stage.csv"
_get_multi_stage_stats_header() = ["Iteration_Number", "Seconds", "Upper_Bound", "Lower_Bound", "Relative_Gap"]

@doc raw"""
function write_multi_stage_stats(outpath::String, stats_d::Dict)
Expand All @@ -10,22 +13,89 @@ inputs:
"""
function write_multi_stage_stats(outpath::String, stats_d::Dict)

filename = _get_multi_stage_stats_filename()

# don't overwrite existing file
isfile(joinpath(outpath, filename)) && return nothing

times_a = stats_d["TIMES"] # Time (seconds) of each iteration
upper_bounds_a = stats_d["UPPER_BOUNDS"] # Upper bound of each iteration
lower_bounds_a = stats_d["LOWER_BOUNDS"] # Lower bound of each iteration

# Create an array of numbers 1 through total number of iterations
iteration_count_a = collect(1:length(times_a))

realtive_gap_a = (upper_bounds_a .- lower_bounds_a) ./ lower_bounds_a

# Construct dataframe where first column is iteration number, second is iteration time
df_stats = DataFrame(Iteration_Number=iteration_count_a,
Seconds=times_a,
Upper_Bound=upper_bounds_a,
Lower_Bound=lower_bounds_a,
Relative_Gap=realtive_gap_a)
header = _get_multi_stage_stats_header()
df_stats = DataFrame(header .=> [iteration_count_a, times_a, upper_bounds_a, lower_bounds_a, realtive_gap_a])

CSV.write(joinpath(outpath, filename), df_stats)
return nothing
end

CSV.write(joinpath(outpath, "stats_multi_stage.csv"), df_stats)
@doc raw"""
create_multi_stage_stats_file(outpath::String)
Create an empty CSV file in the specified output directory with the filename `stats_multi_stage.csv`.
The file contains the columns defined in `_get_multi_stage_stats_header()`.
The function first generates the filename and header using `_get_multi_stage_stats_filename()` and
`_get_multi_stage_stats_header()` respectively. It then creates a DataFrame with column names as headers and
writes it into a CSV file in the specified output directory.
# Arguments
- `outpath::String`: The output directory where the statistics file will be written.
# Returns
- Nothing. A CSV file is written to the `outpath`.
"""
function create_multi_stage_stats_file(outpath::String)
filename = _get_multi_stage_stats_filename()
header = _get_multi_stage_stats_header()
df_stats = DataFrame([col_name => Float64[] for col_name in header])
CSV.write(joinpath(outpath, filename), df_stats)
end

@doc raw"""
update_multi_stage_stats_file(outpath::String, ic::Int64, upper_bound::Float64, lower_bound::Float64, iteration_time::Float64; new_row::Bool=false)
Update a multi-stage statistics file.
# Arguments
- `outpath::String`: The output directory where the statistics file will be written.
- `ic::Int64`: The iteration count.
- `upper_bound::Float64`: The upper bound value.
- `lower_bound::Float64`: The lower bound value.
- `iteration_time::Float64`: The iteration time value.
- `new_row::Bool=false`: Optional argument to determine whether to append a new row (if true) or update the current row (if false).
The function first checks if the file exists. If it does not, it creates a new one.
Then, it reads the statistics from the existing file into a DataFrame.
It calculates the relative gap based on the upper and lower bounds, and either appends a new row or updates the current row based on the `new_row` argument.
Finally, it writes the updated DataFrame back to the file.
# Returns
- Nothing. A CSV file is updated or created at the `outpath`.
"""
function update_multi_stage_stats_file(outpath::String, ic::Int64, upper_bound::Float64, lower_bound::Float64, iteration_time::Float64; new_row::Bool=false)
filename = _get_multi_stage_stats_filename()

# If the file does not exist, create it
if !isfile(joinpath(outpath, filename))
create_multi_stage_stats_file(outpath)
end

df_stats = CSV.read(joinpath(outpath, filename), DataFrame, types=Float64)

relative_gap = (upper_bound - lower_bound) / lower_bound

new_values = [ic, iteration_time, upper_bound, lower_bound, relative_gap]

# If new_row is true, append the new values to the end of the dataframe
# otherwise, update the row at index ic
new_row ? push!(df_stats, new_values) : (df_stats[ic, :] = new_values)

CSV.write(joinpath(outpath, filename), df_stats)
return nothing
end
7 changes: 7 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ using Test
@test isa(inputs_gen["VRE"], Int64)
=#
end

# Test writing outputs
@testset "Writing outputs " begin
for test_file in filter!(x -> endswith(x, ".jl"), readdir("writing_outputs"))
include("writing_outputs/$test_file")
end
end
99 changes: 99 additions & 0 deletions test/writing_outputs/test_writing_stats_ms.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
module TestWritingStatsMs

using Test
using CSV, DataFrames
using GenX


# create temporary directory for testing
mkpath("writing_outputs/multi_stage_stats_tmp")
outpath = "writing_outputs/multi_stage_stats_tmp"
filename = GenX._get_multi_stage_stats_filename()

function test_header()
# Note: if this test fails, it means that the header in the function _get_multi_stage_stats_header() has been changed.
# Make sure to check that the code is consistent with the new header, and update the test accordingly.
header = GenX._get_multi_stage_stats_header()
@test header == ["Iteration_Number", "Seconds", "Upper_Bound", "Lower_Bound", "Relative_Gap"]
end

function test_skip_existing_file()
touch(joinpath(outpath, filename))
# If the file already exists, don't overwrite it
write_multi_stage_stats = GenX.write_multi_stage_stats(outpath, Dict())
@test isnothing(write_multi_stage_stats)
rm(joinpath(outpath, filename))
end

function test_write_multi_stage_stats(iter::Int64 = 10)
# test writing stats to file for `iter` number of iterations
times_a, upper_bounds_a, lower_bounds_a = rand(iter), rand(iter), rand(iter)
stats_d = Dict("TIMES" => times_a, "UPPER_BOUNDS" => upper_bounds_a, "LOWER_BOUNDS" => lower_bounds_a)

@test isnothing(GenX.write_multi_stage_stats(outpath, stats_d))
df_stats = CSV.read(joinpath(outpath, filename), DataFrame)
header = GenX._get_multi_stage_stats_header()
@test size(df_stats) == (iter, length(header))
for i in 1:iter
test_stats_d(df_stats, i, times_a[i], upper_bounds_a[i], lower_bounds_a[i], (upper_bounds_a[i] - lower_bounds_a[i]) / lower_bounds_a[i])
end
rm(joinpath(outpath, filename))
end

function test_create_multi_stage_stats_file()
GenX.create_multi_stage_stats_file(outpath)
df_stats = CSV.read(joinpath(outpath, filename), DataFrame)
@test size(df_stats, 1) == 0
@test size(df_stats, 2) == 5
@test names(df_stats) == GenX._get_multi_stage_stats_header()
rm(joinpath(outpath, filename))
end

function test_update_multi_stage_stats_file(iter::Int64 = 10)
# test updating the stats file with new values
header = GenX._get_multi_stage_stats_header()
GenX.create_multi_stage_stats_file(outpath)
lower_bound = rand()
iteration_time = rand()
for i in 1:iter
# upper bound is updated
upper_bound = rand()
GenX.update_multi_stage_stats_file(outpath, i, upper_bound, lower_bound, iteration_time, new_row=true)
df_stats = CSV.read(joinpath(outpath, filename), DataFrame)
test_stats_d(df_stats, i, iteration_time, upper_bound, lower_bound, (upper_bound - lower_bound) / lower_bound)
# lower bound is updated
lower_bound = rand()
GenX.update_multi_stage_stats_file(outpath, i, upper_bound, lower_bound, iteration_time)
df_stats = CSV.read(joinpath(outpath, filename), DataFrame)
test_stats_d(df_stats, i, iteration_time, upper_bound, lower_bound, (upper_bound - lower_bound) / lower_bound)
# iteration time is updated
iteration_time = rand()
GenX.update_multi_stage_stats_file(outpath, i, upper_bound, lower_bound, iteration_time)
df_stats = CSV.read(joinpath(outpath, filename), DataFrame)
test_stats_d(df_stats, i, iteration_time, upper_bound, lower_bound, (upper_bound - lower_bound) / lower_bound)
# test size
@test size(df_stats) == (i, length(header))
end
rm(joinpath(outpath, filename))
end

function test_stats_d(df_stats, i, iteration_time, upper_bound, lower_bound, relative_gap)
header = GenX._get_multi_stage_stats_header()
@test df_stats[i, header[1]] == i
@test df_stats[i, header[2]] == iteration_time
@test df_stats[i, header[3]] == upper_bound
@test df_stats[i, header[4]] == lower_bound
@test df_stats[i, header[5]] == relative_gap
end

@testset "Test writing multi-stage stats" begin
test_header()
test_skip_existing_file()
test_write_multi_stage_stats()
test_create_multi_stage_stats_file()
test_update_multi_stage_stats_file()
end

rm(outpath)

end # module TestWritingStatsMs

0 comments on commit e45ed43

Please sign in to comment.