• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GenXProject / GenX / #385

14 Feb 2024 08:23PM UTC coverage: 0.079%. First build
#385

Pull #628

travis-ci

Pull Request #628: Bug Fix for correct tracking of current investment stage by SDDP

0 of 1 new or added line in 1 file covered. (0.0%)

3 of 3810 relevant lines covered (0.08%)

0.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/multi_stage/dual_dynamic_programming.jl
1
@doc raw"""
2
    configure_ddp_dicts(setup::Dict, inputs::Dict)
3

4
This function instantiates Dictionary objects containing the names of linking expressions, constraints, and variables used in multi-stage modeling.
5

6
inputs:
7

8
* setup - Dictionary object containing GenX settings and key parameters.
9
* inputs – Dictionary of inputs for each model period, generated by the load\_inputs() method.
10

11
returns:
12

13
* start\_cap\_d – Dictionary which contains linking expression names as keys and linking constraint names as values, used for setting the end capacity in stage $p$ to the starting capacity in stage $p+1$.
14
* cap\_track\_d – Dictionary which contains linking variable names as keys and linking constraint names as values, used for enforcing endogenous retirements.
15
"""
16
function configure_ddp_dicts(setup::Dict, inputs::Dict)
×
17

18
    # start_cap_d dictionary contains key-value pairs of available capacity investment expressions
19
    # as keys and their corresponding linking constraints as values
20
    start_cap_d = Dict([(Symbol("eTotalCap"), Symbol("cExistingCap"))])
×
21

22
    if !isempty(inputs["STOR_ALL"])
×
23
        start_cap_d[Symbol("eTotalCapEnergy")] = Symbol("cExistingCapEnergy")
×
24
    end
25

26
    if !isempty(inputs["STOR_ASYMMETRIC"])
×
27
        start_cap_d[Symbol("eTotalCapCharge")] = Symbol("cExistingCapCharge")
×
28
    end
29

30
    if setup["NetworkExpansion"] == 1 && inputs["Z"] > 1
×
31
        start_cap_d[Symbol("eAvail_Trans_Cap")] = Symbol("cExistingTransCap")
×
32
    end
33

34
    if !isempty(inputs["VRE_STOR"])
35
        if !isempty(inputs["VS_DC"])
36
            start_cap_d[Symbol("eTotalCap_DC")] = Symbol("cExistingCapDC")
×
37
        end
38

×
39
        if !isempty(inputs["VS_SOLAR"])
×
40
            start_cap_d[Symbol("eTotalCap_SOLAR")] = Symbol("cExistingCapSolar")
41
        end
42

×
43
        if !isempty(inputs["VS_WIND"])
×
44
            start_cap_d[Symbol("eTotalCap_WIND")] = Symbol("cExistingCapWind")
45
        end
46

×
47
        if !isempty(inputs["VS_ELEC"])
48
            start_cap_d[Symbol("eTotalCap_ELEC")] = Symbol("cExistingCapElec")
49
        end
50

51
        if !isempty(inputs["VS_STOR"])
52
            start_cap_d[Symbol("eTotalCap_STOR")] = Symbol("cExistingCapEnergy_VS")
53
        end
54

55
        if !isempty(inputs["VS_ASYM_DC_DISCHARGE"])
56
            start_cap_d[Symbol("eTotalCapDischarge_DC")] = Symbol("cExistingCapDischargeDC")
57
        end
58

59
        if !isempty(inputs["VS_ASYM_DC_CHARGE"])
60
            start_cap_d[Symbol("eTotalCapCharge_DC")] = Symbol("cExistingCapChargeDC")
61
        end
62

63
        if !isempty(inputs["VS_ASYM_AC_DISCHARGE"])
64
            start_cap_d[Symbol("eTotalCapDischarge_AC")] = Symbol("cExistingCapDischargeAC")
65
        end
66

×
67
        if !isempty(inputs["VS_ASYM_AC_CHARGE"])
68
            start_cap_d[Symbol("eTotalCapCharge_AC")] = Symbol("cExistingCapChargeAC")
×
69
        end
×
70
    end
×
71

×
72
    # This dictionary contains the endogenous retirement constraint name as a key,
73
    # and a tuple consisting of the associated tracking array constraint and variable as the value
×
74
    cap_track_d = Dict([(Symbol("vCAPTRACK"), Symbol("cCapTrack"))])
75

×
76
    if !isempty(inputs["STOR_ALL"])
77
        cap_track_d[Symbol("vCAPTRACKENERGY")] = Symbol("cCapTrackEnergy")
×
78
    end
×
79

×
80
    if !isempty(inputs["STOR_ASYMMETRIC"])
×
81
        cap_track_d[Symbol("vCAPTRACKCHARGE")] = Symbol("cCapTrackCharge")
×
82
    end
83

84
    if !isempty(inputs["VRE_STOR"])
×
NEW
85
        if !isempty(inputs["VS_DC"])
×
86
            cap_track_d[Symbol("vCAPTRACKDC")] = Symbol("cCapTrackDC")
×
87
        end
88

89
        if !isempty(inputs["VS_SOLAR"])
90
            cap_track_d[Symbol("vCAPTRACKSOLAR")] = Symbol("cCapTrackSolar")
×
91
        end
92

93
        if !isempty(inputs["VS_WIND"])
×
94
            cap_track_d[Symbol("vCAPTRACKWIND")] = Symbol("cCapTrackWind")
×
95
        end
×
96

97
        if !isempty(inputs["VS_ELEC"])
98
            cap_track_d[Symbol("vCAPTRACKELEC")] = Symbol("cCapTrackElec")
×
99
        end
×
100

×
101
        if !isempty(inputs["VS_STOR"])
×
102
            cap_track_d[Symbol("vCAPTRACKENERGY_VS")] = Symbol("cCapTrackEnergy_VS")
×
103
        end
104

105
        if !isempty(inputs["VS_ASYM_DC_DISCHARGE"])
×
106
            cap_track_d[Symbol("vCAPTRACKDISCHARGEDC")] = Symbol("cCapTrackDischargeDC")
107
        end
108

×
109
        if !isempty(inputs["VS_ASYM_DC_CHARGE"])
110
            cap_track_d[Symbol("vCAPTRACKCHARGEDC")] = Symbol("cCapTrackChargeDC")
×
111
        end
112

×
113
        if !isempty(inputs["VS_ASYM_AC_DISCHARGE"])
×
114
            cap_track_d[Symbol("vCAPTRACKDISCHARGEAC")] = Symbol("cCapTrackDischargeAC")
×
115
        end
×
116

×
117
        if !isempty(inputs["VS_ASYM_AC_CHARGE"])
×
118
            cap_track_d[Symbol("vCAPTRACKCHARGEAC")] = Symbol("cCapTrackChargeAC")
119
        end
×
120
    end
×
121

×
122
    return start_cap_d, cap_track_d
123
end
×
124

125
@doc raw"""
126
        run_ddp(outpath::AbstractString, models_d::Dict, setup::Dict, inputs_d::Dict)
×
127

×
128
This function run the dual dynamic programming (DDP) algorithm, as described in [Pereira and Pinto (1991)](https://doi.org/10.1007/BF01582895), and more recently, [Lara et al. (2018)](https://doi.org/10.1016/j.ejor.2018.05.039). Note that if the algorithm does not converge within 10,000 (currently hardcoded) iterations, this function will return models with sub-optimal solutions. However, results will still be printed as if the model is finished solving. This sub-optimal termination is noted in the output with the 'Exiting Without Covergence!' message.
×
129

×
130
inputs:
×
131

132
  * outpath - string for where to write results
133
  * models\_d – Dictionary which contains a JuMP model for each model period.
134
  * setup - Dictionary object containing GenX settings and key parameters.
135
  * inputs\_d – Dictionary of inputs for each model stage, generated by the load\_inputs() method.
×
136

×
137
returns:
×
138

×
139
  * models\_d – Dictionary which contains a JuMP model for each model stage, modified by this method.
140
  * stats\_d – Dictionary which contains the run time, upper bound, and lower bound of each DDP iteration.
141
  * inputs\_d – Dictionary of inputs for each model stage, generated by the load\_inputs() method, modified by this method.
×
142
"""
143
function run_ddp(outpath::AbstractString, models_d::Dict, setup::Dict, inputs_d::Dict)
×
144
    settings_d = setup["MultiStageSettingsDict"]
×
145
    num_stages = settings_d["NumStages"]  # Total number of investment planning stages
×
146
    EPSILON = settings_d["ConvergenceTolerance"] # Tolerance
147
    @assert settings_d["Myopic"] == 0 "Myopic must be set to 0 for the DDP algorithm. Update your GenX settings."
148

×
149
    start_cap_d, cap_track_d = configure_ddp_dicts(setup, inputs_d[1])
150

151
    times_a = [] # Array to store the total time of each iteration
×
152
    upper_bounds_a = [] # Array to store the upper bound of each iteration
153
    lower_bounds_a = [] # Array to store the lower bound of each iteration
154
    stats_d = Dict() # Dictionary to store the statistics (total time, upper bound, and lower bound for each iteration)
×
155
    stats_d["TIMES"] = times_a
×
156
    stats_d["UPPER_BOUNDS"] = upper_bounds_a
157
    stats_d["LOWER_BOUNDS"] = lower_bounds_a
158

159
    # Step a.i) Initialize cost-to-go function for t = 1:num_stages
160
    for t in 1:num_stages
×
161
        settings_d["CurStage"] = t
×
162
        models_d[t] = initialize_cost_to_go(settings_d, models_d[t], inputs_d[t])
×
163
    end
×
164

×
165
    # Step a.ii) Set objective upper bound
×
166
    global z_upper = Inf
167

×
168
    # Step b.i) Solve the approximate first-stage problem
×
169
    println("***********")
×
170
    println("Solving First Stage Problem")
×
171
    println("***********")
172

173
    t = 1 # Stage = 1
174
    solve_time_d = Dict()
175
    ddp_prev_time = time() # Begin tracking time of each iteration
×
176
    models_d[t], solve_time_d[t] = solve_model(models_d[t], setup)
×
177
    inputs_d[t]["solve_time"] = solve_time_d[t]
×
178

179
    # Step c.i) Initialize the lower bound, equal to the objective function value for the first period in the first iteration
180
    global z_lower = objective_value(models_d[t])
181

×
182
    # Step c.ii) If the relative difference between upper and lower bounds are small, break loop
×
183
    ic = 0 # Iteration Counter
184
    while ((z_upper - z_lower) / z_lower > EPSILON)
185
        ic = ic + 1 # Increase iteration counter by 1
×
186

187
        if (ic > 10000)
188
            println("***********")
×
189
            println("Exiting Without Covergence!")
190
            println(string("Upper Bound = ", z_upper))
×
191
            println(string("Lower Bound = ", z_lower))
×
192
            println("***********")
×
193

194
            return models_d, stats_d, inputs_d
195
        end
×
196

197
        println("***********")
198
        println(string("Iteration Number: ", ic))
×
199
        println(string("Upper Bound = ", z_upper))
×
200
        println(string("Lower Bound = ", z_lower))
201
        println("***********")
202

203
        # Step d) Forward pass for t = 1:num_stages
×
204
        ## For first iteration we dont need to solve forward pass for first stage (we did that already above),
×
205
        ## but we need to update forward pass solution for the first stage for subsequent iterations
206
        if ic > 1
207
            t = 1 #  update forward pass solution for the first stage
×
208
            models_d[t], solve_time_d[t] = solve_model(models_d[t], setup)
×
209
            inputs_d[t]["solve_time"] = solve_time_d[t]
×
210
        end
211
        ## Forward pass for t=2:num_stages
212
        for t in 2:num_stages
×
213
            println("***********")
×
214
            println(string("Forward Pass t = ", t))
×
215
            println("***********")
×
216

×
217
            # Step d.i) Fix initial investments for model at time t given optimal solution for time t-1
218
            models_d[t] = fix_initial_investments(models_d[t - 1],
219
                models_d[t],
220
                start_cap_d,
221
                inputs_d[t])
×
222

×
223
            # Step d.ii) Fix capacity tracking variables for endogenous retirements
×
224
            models_d[t] = fix_capacity_tracking(models_d[t - 1],
225
                models_d[t],
×
226
                cap_track_d,
×
227
                t)
×
228

×
229
            # Step d.iii) Solve the model at time t
230
            models_d[t], solve_time_d[t] = solve_model(models_d[t], setup)
231
            inputs_d[t]["solve_time"] = solve_time_d[t]
×
232

233
        end
234

×
235
        # Step e) Calculate the new upper bound
236
        z_upper_temp = 0
237
        for t in 1:num_stages
×
238
            z_upper_temp = z_upper_temp +
×
239
                           (objective_value(models_d[t]) - value(models_d[t][:vALPHA]))
240
        end
241

242
        # If the upper bound decreased, set it as the new upper bound
×
243
        if z_upper_temp < z_upper
×
244
            z_upper = z_upper_temp
×
245
        end
246

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

250
        # Step f) Backward pass for t = num_stages:2
251
        for t in num_stages:-1:2
252
            println("***********")
253
            println(string("Backward Pass t = ", t))
254
            println("***********")
255

256
            # Step f.i) Add a cut to the previous time step using information from the current time step
257
            models_d[t - 1] = add_cut(models_d[t - 1],
258
                models_d[t],
259
                start_cap_d,
260
                cap_track_d)
×
261

262
            # Step f.ii) Solve the model with the additional cut at time t-1
×
263
            models_d[t - 1], solve_time_d[t - 1] = solve_model(models_d[t - 1], setup)
264
            inputs_d[t - 1]["solve_time"] = solve_time_d[t - 1]
×
265
        end
×
266

×
267
        # Step g) Recalculate lower bound and go back to c)
×
268
        z_lower = objective_value(models_d[1])
×
269
        append!(lower_bounds_a, z_lower) # Store current iteration lower bound
270
        update_multi_stage_stats_file(outpath, ic, z_upper, z_lower, NaN)
×
271

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

277
        ddp_prev_time = time()
278
    end
279

280
    println("***********")
281
    println("Successful Convergence!")
282
    println(string("Upper Bound = ", z_upper))
283
    println(string("Lower Bound = ", z_lower))
284
    println("***********")
285

286
    ### STEP I) One final forward pass to guarantee convergence
287
    # Forward pass for t = 1:num_stages
288
    t = 1 #  update forward pass solution for the first stage
289
    models_d[t], solve_time_d[t] = solve_model(models_d[t], setup)
×
290
    inputs_d[t]["solve_time"] = solve_time_d[t]
291
    ## Forward pass for t=2:num_stages
×
292
    for t in 2:num_stages
293
        println("***********")
294
        println(string("Final Forward Pass t = ", t))
295
        println("***********")
×
296

×
297
        # Step d.i) Fix initial investments for model at time t given optimal solution for time t-1
×
298
        models_d[t] = fix_initial_investments(models_d[t - 1],
299
            models_d[t],
300
            start_cap_d,
×
301
            inputs_d[t])
302

303
        # Step d.ii) Fix capacity tracking variables for endogenous retirements
304
        models_d[t] = fix_capacity_tracking(models_d[t - 1], models_d[t], cap_track_d, t)
×
305

306
        # Step d.iii) Solve the model at time t
307
        models_d[t], solve_time_d[t] = solve_model(models_d[t], setup)
308
        inputs_d[t]["solve_time"] = solve_time_d[t]
309
    end
310
    ##### END of final forward pass
311

312
    return models_d, stats_d, inputs_d
313
end
314

315
@doc raw"""
316
        fix_initial_investments(EP_prev::Model, EP_cur::Model, start_cap_d::Dict)
317

318
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.
319

320
inputs:
321

322
  * EP\_prev - JuMP model from the previous model stage $p-1$.
323
  * EP\_cur - JuMP model from the current model stage $p$.
×
324
  * start\_cap\_d – Dictionary which contains key-value pairs of available capacity investment expression names (as Symbols) as keys and their corresponding linking constraint names (as Symbols) as values.
325

326
returns: JuMP model with updated linking constraints.
327
"""
×
328
function fix_initial_investments(EP_prev::Model,
329
        EP_cur::Model,
330
        start_cap_d::Dict,
331
        inputs_d::Dict)
×
332
    ALL_CAP = union(inputs_d["RET_CAP"], inputs_d["NEW_CAP"]) # Set of all resources subject to inter-stage capacity tracking
×
333

334
    # start_cap_d dictionary contains the starting capacity expression name (e) as a key,
×
335
    # and the associated linking constraint name (c) as a value
×
336
    for (e, c) in start_cap_d
337
        for y in keys(EP_cur[c])
338
            # Set the right hand side value of the linking initial capacity constraint in the current stage to the value of the available capacity variable solved for in the previous stages
339
            if c == :cExistingTransCap
×
340
                set_normalized_rhs(EP_cur[c][y], value(EP_prev[e][y]))
341
            else
×
342
                if y[1] in ALL_CAP # extract resource integer index value from key
343
                    set_normalized_rhs(EP_cur[c][y], value(EP_prev[e][y]))
×
344
                end
345
            end
346
        end
347
    end
348
    return EP_cur
×
349
end
350

351
@doc raw"""
352
        fix_capacity_tracking(EP_prev::Model, EP_cur::Model, cap_track_d::Dict, cur_stage::Int)
353

354
This function sets the right hand side values of the new and retired capacity tracking linking constraints in the current stage $p$ to the realized values of the new and retired capacity tracking linking variables from the previous stage $p-1$ as part of the forward pass.
355
where tracking linking variables are defined variables for tracking, linking and passing realized expansion and retirement of capacities of each stage to the next stage.
356
Tracking linking variables are each defined in endogenous\_retirement\_discharge, endogenous\_retirement\_energy, and endogenous\_retirement\_charge functions. Three examples are "vCAPTRACK", "vCAPTRACKCHARGE", and ""vCAPTRACKENERGY"
357

358
inputs:
359

360
  * EP\_prev - JuMP model from the previous model stage $p-1$.
361
  * EP\_cur - JuMP model from the current model stage $p$.
362
  * cap\_track\_d – Dictionary which contains key-value pairs of capacity addition and retirement tracking variable names (as Symbols) as keys and their corresponding linking constraint names (as Symbols) as values.
363
  * cur\_period – Int representing the current model stage $p$.
×
364

365
returns: JuMP model with updated linking constraints.
×
366
"""
367
function fix_capacity_tracking(EP_prev::Model,
×
368
        EP_cur::Model,
369
        cap_track_d::Dict,
370
        cur_stage::Int)
371

372
    # cap_track_d dictionary contains the endogenous retirement tracking array variable name (v) as a key,
373
    # and the associated linking constraint name (c) as a value
×
374
    for (v, c) in cap_track_d
375

376
        # Tracking variables and constraints for retired capacity are named identicaly to those for newly
×
377
        # built capacity, except have the prefex "vRET" and "cRet", accordingly
×
378
        rv = Symbol("vRET", string(v)[2:end]) # Retired capacity tracking variable name (rv)
379
        rc = Symbol("cRet", string(c)[2:end]) # Retired capacity tracking constraint name (rc)
380

381
        for i in keys(EP_cur[c])
×
382
            i = i[1] # Extract integer index value from keys tuple - corresponding to generator index
383

384
            # For all previous stages, set the right hand side value of the tracking constraint in the current
×
385
            # stage to the value of the tracking constraint observed in the previous stage
386
            for p in 1:(cur_stage - 1)
387
                # Tracking newly buily capacity over all previous stages
388
                JuMP.set_normalized_rhs(EP_cur[c][i, p], value(EP_prev[v][i, p]))
389
                # Tracking retired capacity over all previous stages
390
                JuMP.set_normalized_rhs(EP_cur[rc][i, p], value(EP_prev[rv][i, p]))
391
            end
×
392
        end
393
    end
394

×
395
    return EP_cur
×
396
end
397

398
@doc raw"""
399
        add_cut(EP_cur::Model, EP_next::Model, start_cap_d::Dict, cap_track_d::Dict)
×
400

401
inputs:
×
402

×
403
  * EP\_cur - JuMP model from the current model stage $p$.
404
  * EP\_next - JuMP model from the next model stage $p+1$..
405
  * start\_cap\_d – Dictionary which contains key-value pairs of available capacity investment expression names (as Symbols) as keys and their corresponding linking constraint names (as Symbols) as values.
×
406
  * cap\_track\_d – Dictionary which contains key-value pairs of capacity addition and retirement tracking variable names (as Symbols) as keys and their corresponding linking constraint names (as Symbols) as values.
407

408
returns: JuMP expression representing a sum of Benders cuts for linking capacity investment variables to be added to the cost-to-go function.
×
409
"""
410
function add_cut(EP_cur::Model, EP_next::Model, start_cap_d::Dict, cap_track_d::Dict)
411
    next_obj_value = objective_value(EP_next) # Get the objective function value for the next investment planning stage
412

×
413
    eRHS = @expression(EP_cur, 0) # Initialize RHS of cut to 0
414

×
415
    # Generate cut components for investment decisions
416

417
    # start_cap_d dictionary contains the starting capacity expression name (e) as a key,
418
    # and the associated linking constraint name (c) as a value
419
    for (e, c) in start_cap_d
420

421
        # Continue if nothing to add to the cut
422
        if isempty(EP_next[e])
423
            continue
424
        end
425

426
        # Generate the cut component
427
        eCurRHS = generate_cut_component_inv(EP_cur, EP_next, e, c)
428

429
        # Add the cut component to the RHS
430
        eRHS = eRHS + eCurRHS
431
    end
432

433
    # Generate cut components for endogenous retirements.
434

435
    # cap_track_d dictionary contains the endogenous retirement tracking array variable name (v) as a key,
436
    # and the associated linking constraint name (c) as a value
437
    for (v, c) in cap_track_d
×
438

439
        # Continue if nothing to add to the cut
×
440
        if isempty(EP_next[c])
×
441
            continue
×
442
        end
443

×
444
        # Generate the cut component for new capacity
×
445
        eCurRHS_cap = generate_cut_component_track(EP_cur, EP_next, v, c)
×
446

447
        rv = Symbol("vRET", string(v)[2:end]) # Retired capacity tracking variable (rv)
×
448
        rc = Symbol("cRet", string(c)[2:end]) # Retired capacity tracking constraint (rc)
×
449

×
450
        # Generate the cut component for retired capacity
451
        eCurRHS_ret = generate_cut_component_track(EP_cur, EP_next, rv, rc)
452

×
453
        # Add the cut component to the RHS
454
        eRHS = eRHS + eCurRHS_cap + eCurRHS_ret
×
455
    end
456

457
    # Add the cut to the model
458
    @constraint(EP_cur, EP_cur[:vALPHA]>=next_obj_value - eRHS)
459

460
    return EP_cur
461
end
462

463
@doc raw"""
464
        generate_cut_component_inv(EP_cur::Model, EP_next::Model, expr_name::Symbol, constr_name::Symbol)
465

466
This function generates Bender's cut expressions for total new or retired capacity tracking linking variables in the form:
467
```math
468
\begin{aligned}
469
        \mu_{next}^{\top}(\hat{x}_{cur} - x_{cur})
470
\end{aligned}
471
```
472
where $\mu_{next}$ is a vector of dual values of the linking constraints defined by constr\_name in EP\_next, $\hat{x}_{cur}$ is a vector of realized values from the forward pass of the new or retired capacity tracking linking variables var\_name from EP\_cur, and $x_{cur}$ is a vector of unrealized new or retired capacity tracking linking variables from EP\_cur.
473

474
inputs:
475

476
  * EP\_cur - JuMP model from the current model stage $p$.
477
  * EP\_next - JuMP model from the next model stage $p+1$.
×
478
  * var\_name – Symbol representing the name of a JuMP variable array which contains total new or retired capacity tracking linking variables.
479
  * constr\_name – Symbol representing the name of the array of linking JuMP constraints which contain total new or retired capacity tracking linking variables.
×
480

×
481
returns: JuMP expression representing a sum of Benders cuts for linking capacity investment variables to be added to the cost-to-go function.
×
482
"""
483
function generate_cut_component_track(EP_cur::Model,
×
484
        EP_next::Model,
485
        var_name::Symbol,
×
486
        constr_name::Symbol)
×
487
    next_dual_value = Float64[]
×
488
    cur_inv_value = Float64[]
489
    cur_inv_var = []
490

×
491
    for k in keys(EP_next[constr_name])
492
        y = k[1] # Index representing resource
×
493
        p = k[2] # Index representing stage
494

495
        push!(next_dual_value, dual(EP_next[constr_name][y, p]))
496
        push!(cur_inv_value, value(EP_cur[var_name][y, p]))
497
        push!(cur_inv_var, EP_cur[var_name][y, p])
498
    end
499

500
    eCutComponent = @expression(EP_cur,
501
        dot(next_dual_value, (cur_inv_value .- cur_inv_var)))
502

503
    return eCutComponent
504
end
505

506
@doc raw"""
507
        generate_cut_component_inv(EP_cur::Model, EP_next::Model, expr_name::Symbol, constr_name::Symbol)
508

509
This function generates Bender's cut expressions for linking capacity investment variable expression in the form:
510
```math
511
\begin{aligned}
512
        \mu_{next}^{\top}(\hat{x}_{cur} - x_{cur})
513
\end{aligned}
514
```
515
where $\mu_{next}$ is a vector of dual values of the linking constraints defined by constr\_name in EP\_next, $\hat{x}_{cur}$ is a vector of realized values from the forward pass of the linking capacity investment variable expressions expr\_name from EP\_cur, and $x_{cur}$ is a vector of unrealized linking capacity investment variable expressions from EP\_cur. inputs:
516

517
inputs:
518

519
  * EP\_cur - JuMP model from the current model stage $p$, solved in the forward pass.
520
  * EP\_next - JuMP model from the next model stage $p+1$, solved in the forward pass.
521
  * expr\_name – Symbol representing the name of a JuMP expression array which contains linking capacity investment variables.
522
  * constr\_name – Symbol representing the name of the array of linking JuMP constraints which contain the linking capacity investment variables.
523

524
returns: JuMP expression representing a sum of Benders cuts for linking capacity investment variables to be added to the cost-to-go function.
525
"""
526
function generate_cut_component_inv(EP_cur::Model,
527
        EP_next::Model,
528
        expr_name::Symbol,
529
        constr_name::Symbol)
530
    next_dual_value = Float64[]
531
    cur_inv_value = Float64[]
×
532
    cur_inv_var = []
533

×
534
    for y in keys(EP_next[constr_name])
×
535
        push!(next_dual_value, dual(EP_next[constr_name][y]))
×
536
        push!(cur_inv_value, value(EP_cur[expr_name][y]))
×
537
        push!(cur_inv_var, EP_cur[expr_name][y])
×
538
    end
539

540
    eCutComponent = @expression(EP_cur,
541
        dot(next_dual_value, (cur_inv_value .- cur_inv_var)))
542

×
543
    return eCutComponent
544
end
×
545

546
@doc raw"""
×
547
        initialize_cost_to_go(settings_d::Dict, EP::Model)
548

×
549
This function scales the model objective function so that costs are consistent with multi-stage modeling and introduces a cost-to-go function variable to the objective function.
×
550

551
The updated objective function $OBJ^{*}$ returned by this method takes the form:
552
```math
×
553
\begin{aligned}
554
    OBJ^{*} = DF * OPEXMULT * OBJ + \alpha
555
\end{aligned}
556
```
557
where $OBJ$ is the original objective function. $OBJ$ is scaled by two terms. The first is a discount factor, which discounts costs associated with the model stage $p$ to year-0 dollars:
558
```math
559
\begin{aligned}
560
    DF = \frac{1}{(1+WACC)^{\sum^{(p-1)}_{k=0}L_{k}}}
561
\end{aligned}
562
```
563
where $WACC$ is the weighted average cost of capital, and $L_{p}$ is the length of each stage in years (both set in multi\_stage\_settings.yml)
564

565
The second term is a discounted sum of annual operational expenses incurred each year of a multi-year model stage:
566
```math
567
\begin{aligned}
568
    & OPEXMULT = \sum^{L}_{l=1}\frac{1}{(1+WACC)^{l-1}}
569
\end{aligned}
570
```
571
Note that although the objective function contains investment costs, which occur only once and thus do not need to be scaled by OPEXMULT, these costs are multiplied by a factor of $\frac{1}{WACC}$ before being added to the objective function in investment\_discharge\_multi\_stage(), investment\_charge\_multi\_stage(), investment\_energy\_multi\_stage(), and transmission\_multi\_stage(). Thus, this step scales these costs back to their correct value.
572

573
The cost-to-go function $\alpha$ represents an approximation of future costs given the investment and retirement decisions in the current stage. It is constructed through the addition of cuts to the cost-to-go function $\alpha$ during the backwards pass.
574

575
inputs:
576

577
  * settings\_d - Dictionary containing settings dictionary configured in the multi-stage settings file multi\_stage\_settings.yml.
578
  * EP – JuMP model.
579

580
returns: JuMP model with updated objective function.
581
"""
582
function initialize_cost_to_go(settings_d::Dict, EP::Model, inputs::Dict)
583
    cur_stage = settings_d["CurStage"] # Current DDP Investment Planning Stage
584
    cum_years = 0
585
    for stage_count in 1:(cur_stage - 1)
586
        cum_years += settings_d["StageLengths"][stage_count]
587
    end
588
    wacc = settings_d["WACC"] # Interest Rate  and also the discount rate unless specified other wise
589
    OPEXMULT = inputs["OPEXMULT"] # OPEX multiplier to count multiple years between two model stages, set in configure_multi_stage_inputs.jl
590

591
    # Overwrite the objective function to include the cost-to-go variable (not in myopic case)
592
    # Multiply discount factor to all terms except the alpha term or the cost-to-go function
593
    # All OPEX terms get an additional adjustment factor
594
    DF = 1 / (1 + wacc)^(cum_years)  # Discount factor applied all to costs in each stage ###
595
    # Initialize the cost-to-go variable
596
    @variable(EP, vALPHA>=0)
597
    @objective(EP, Min, DF * OPEXMULT * EP[:eObj]+vALPHA)
598

599
    return EP
600
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc