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 2668eae
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 57 deletions.
28 changes: 27 additions & 1 deletion docs/src/write_outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,33 @@ Pages = ["write_esr_revenue.jl"]
Modules = [GenX]
Pages = ["write_net_revenue.jl"]
```
## Write Settings files

## Write Co-Located VRE and Storage files
```@docs
GenX.write_vre_stor
GenX.write_vre_stor_capacity
GenX.write_vre_stor_charge
GenX.write_vre_stor_discharge
```

## Write Multi-stage files
```@autodocs
Modules = [GenX]
Pages = ["write_multi_stage_outputs.jl"]
```
```@docs
GenX.write_multi_stage_costs
GenX.write_multi_stage_stats
GenX.write_multi_stage_settings
GenX.write_multi_stage_network_expansion
GenX.write_multi_stage_capacities_discharge
GenX.write_multi_stage_capacities_charge
GenX.write_multi_stage_capacities_energy
GenX.create_multi_stage_stats_file
GenX.update_multi_stage_stats_file
```

## Write maintenance files
```@autodocs
Modules = [GenX]
Pages = ["write_settings.jl"]
Expand Down
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
53 changes: 10 additions & 43 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,42 +237,11 @@ 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)
fix_initial_investments(EP_prev::Model, EP_cur::Model, start_cap_d::Dict)
This function sets the right hand side values of the existing capacity linking constraints in the current stage $p$ to the realized values of the total available end capacity linking variable expressions from the previous stage $p-1$ as part of the forward pass.
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))
iteration_count_a = collect(1:length(upper_bounds_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, "stats_multi_stage.csv"), df_stats)
CSV.write(joinpath(outpath, filename), df_stats)
return nothing
end

@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 readdir("writing_outputs") |> filter(x -> endswith(x, ".jl"))
include("writing_outputs/$test_file")
end
end
Loading

0 comments on commit 2668eae

Please sign in to comment.