def b2_sddp(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, T_rows, D_rows, W_rows, b, d, w, scenarios, prob, z_lower, delta, output_stream = sys.stdout): """Apply the B^2 algorithm for dynamic programming. Solve an infinite horizon stochastic dynamic program using the B^2 algorithm, given the initial data. It is assumed that this is a minimization problem and the constraints are in >= form. The problem is written as: .. math :: \min \{\sum_{t=0}^\infty c^T x_t + h^T y_t : \forall t A x_t + G y_t \ge b + Ty_{t-1}, D x_t \ge d, W y_t \ge w\} Parameters ---------- settings : b2_sddp_settings.B2Settings Algorithmic settings. n1 : int Number of x variables of the problem. n2 : int Number of y variables of the problem. k : int Number of scenarios. m : int Number of rows in the first set of constraints A x + G y >= b + T y_{t-1}. p : int Number of rows in the second set of constraints D x >= d. q : int Number of rows in the third set of constraints W x >= w. x_obj : List[float] Cost coefficient for the x variables. y_obj : List[float] Cost coefficient for the y (state) variables. A_rows : List[List[int], List[float]] The coefficients of the rows of the A matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. G_rows : List[List[int], List[float]] The coefficients of the rows of the G matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. T_rows : List[List[int], List[float]] The coefficients of the rows of the T matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. D_rows : List[List[int], List[float]] The coefficients of the rows of the D matrix in D x >= d. These are in sparse form: indices and elements. W_rows : List[List[int], List[float]] The coefficients of the rows of the W matrix in W y >= w. These are in sparse form: indices and elements. b : List[float] The rhs of the first set of constraints in the master. d : List[float] The rhs of the second set of constraints in the master. w : List[float] The rhs of the third set of constraints in the master. scenarios : List[List[float]] The rhs vector b, d, w for each scenario. prob : List[float] The probability of each scenario. z_lower : List[float] Lower bounds on the objective value of each scenario. delta : float Discount factor, 0 <= delta < 1. output_stream : file Output stream. Must have a 'write' and 'flush' method. Returns ------- (cplex.Cplex, float, float, float, int, float) The master problem after convergence, the best upper bound, the best lower bound, the final gap (as a fraction, i.e. not in percentage point), the final length of the time horizon tau, the total CPU time in seconds. """ assert(isinstance(settings, B2Settings)) assert(len(x_obj) == n1) assert(len(y_obj) == n2) assert(len(A_rows) == m) assert(len(G_rows) == m) assert(len(T_rows) == m) assert(len(D_rows) == p) assert(len(W_rows) == q) assert(len(b) == m) assert(len(d) == p) assert(len(w) == q) assert(len(scenarios) == k) for i in range(k): assert(len(scenarios[i]) == m + p + q) assert(len(prob) == k) assert(len(z_lower) == k) assert(0 <= delta < 1) # Start counting time start_time = time.clock() # Initialize inverse CDF of the probability distribution of the # scenarios inverse_cdf = util.InverseCDF(prob) # Compute column representation of T T_cols = util.row_matrix_to_col_matrix(m, n2, T_rows) # Create the master and slave master = create_master_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, D_rows, W_rows, b, d, w, scenarios, prob, z_lower, delta) slave = create_slave_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, D_rows, W_rows, d, w, prob, z_lower, delta) if (settings.primal_bound_method == 'no_y'): # Create master problem without the y variables pb_prob = pb.create_p_no_y_problem(settings, n1, k, m, p, q, x_obj, A_rows, D_rows, b, d) elif (settings.primal_bound_method == 'fixed_y'): # Create master problem with fixed y variables pb_prob = pb.create_p_fixed_y_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, T_rows, D_rows, W_rows, d, w, scenarios) elif (settings.primal_bound_method == 'p_heur'): # Create the p_heur problem pb_prob = pb.create_p_heur_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, T_rows, D_rows, W_rows, d, w, scenarios) elif (settings.primal_bound_method == 'p_rebalance'): # Create an empty p_heur problem pb_prob = pb.create_p_rebalance() else: raise ValueError('Primal bound method ' + '{:s}'.format(settings.primal_bound_method) + ' not implemented') if (not settings.print_cplex_output): master.set_results_stream(None) slave.set_results_stream(None) pb_prob.set_results_stream(None) else: master.set_results_stream(output_stream) slave.set_results_stream(output_stream) pb_prob.set_results_stream(output_stream) # Store solutions for all forward passes fwpass_sol = list() # Store cost of primal solutions, i.e. sample paths cost_sol = list() # The best primal bound available so far best_ub = cplex.infinity # Is the stopping criterion satisfied? stop = False # Length of the time horizon tau = 0 # Cut activity levels cut_activity = list() # Comput single-stage cost for the infinite tail, if necessary if (settings.primal_bound_method == 'no_y'): ub_tail = pb.cost_tail_no_y(settings, n1, n2, k, m, p, q, T_cols, [0]*n2, 0, delta, scenarios, prob, 0, pb_prob) elif (settings.primal_bound_method == 'fixed_y'): ub_tail = pb.cost_tail_fixed_y(settings, n1, n2, k, m, p, q, T_cols, [0]*n2, 0, delta, scenarios, prob, 0, pb_prob) elif (settings.primal_bound_method == 'p_rebalance'): ub_tail = pb.cost_tail_rebalance(settings, n1, n2, k, m, p, q, x_obj, T_cols, [0]*n2, 0, delta, scenarios, prob,0) # Start the B^2 algorithm! while (not stop): # Increment length of the time horizon if necessary tau = update_time_horizon(settings, tau) print('*** Major iteration with tau {:d}'.format(tau), file = output_stream) output_stream.flush() for num_pass in range(settings.num_passes): x, y, z = forward_pass(settings, n1, n2, k, m, p, q, tau, T_cols, b, d, w, scenarios, prob, inverse_cdf, master, num_pass, cut_activity) # Store all LP solutions fwpass_sol.append((x, y, z)) # Clean up LPs cut_activity = util.purge_cuts(settings, m, p, q, cut_activity, master, slave) # Computation of upper bounds cost_sol.append(pb.cost_primal_sol(x, y, x_obj, y_obj, tau, delta)) # Backward pass: collect Benders cuts cuts = backward_pass(settings, n1, n2, k, m, p, q, tau, T_cols, scenarios, prob, x, y, z, slave, num_pass) util.add_cuts(master, cuts) # -- end time horizon tau # Extend existing sample paths to the desired length for (i, (sol_x, sol_y, sol_z)) in enumerate(fwpass_sol): while (len(sol_x) < tau): # Move forward in time starting from the last solution x, y, z = single_forward_step(settings, n1, n2, k, m, p, q, T_cols, sol_y[-1], scenarios, prob, inverse_cdf, master, cut_activity) sol_x.append(x) sol_y.append(y) sol_z.append(z) # Update upper bound cost_sol[i] += pb.cost_primal_sol([x], [y], x_obj, y_obj, tau, delta, tau - 1) # Backward pass: collect Benders cuts cuts = backward_pass(settings, n1, n2, k, m, p, q, 1, T_cols, scenarios, prob, [x], [y], [z], slave, 0) util.add_cuts(master, cuts) # Primal bound: compute cost for the entire path until infinity cost_path = list() for (i, (sol_x, sol_y, sol_z)) in enumerate(fwpass_sol): if (settings.primal_bound_method == 'no_y'): cost_path.append(pb.cost_tail_no_y(settings, n1, n2, k, m, p, q, T_cols, sol_y[-1], tau, delta, scenarios, prob, ub_tail, pb_prob) + cost_sol[i]) elif (settings.primal_bound_method == 'fixed_y'): cost_path.append(pb.cost_tail_fixed_y(settings, n1, n2, k, m, p, q, T_cols, sol_y[-1], tau, delta, scenarios, prob, ub_tail, pb_prob) + cost_sol[i]) elif (settings.primal_bound_method == 'p_heur'): cost_path.append(pb.cost_tail_p_heur(settings, n1, n2, k, m, p, q, T_cols, sol_y[-1], tau, delta, scenarios, prob, pb_prob) + cost_sol[i]) elif (settings.primal_bound_method == 'p_rebalance'): cost_path.append(pb.cost_tail_rebalance(settings, n1, n2, k, m, p, q, x_obj, T_cols, sol_y[-1], tau, delta, scenarios, prob, ub_tail) + cost_sol[i]) ub = pb.primal_bound(settings, cost_path) # Get current dual bound and compute optimality gap master.linear_constraints.set_rhs([(i, b[i]) for i in range(m)]) master.linear_constraints.set_rhs([(m + i, d[i]) for i in range(p)]) master.linear_constraints.set_rhs([(m + p + i, w[i]) for i in range(q)]) master.solve() lb = master.solution.get_objective_value() if (ub > 0): gap = (ub - lb) / (ub + 1.0e-10) else: gap = (ub - lb) / (-ub - 1.0e-10) # Report progress status #print('Primal bound: {:f} '.format(ub) + #'Dual bound: {:f} '.format(lb) + #'gap {:.2f} %'.format(100*gap)) print('Primal bound: {:f} '.format(ub) + 'Dual bound: {:f} '.format(lb) + 'gap {:.2f} %'.format(100*gap), file = output_stream) output_stream.flush() cpu_time = time.clock() - start_time #Stop when the optimality gap is small enough if (tau > 1): if (tau >= settings.max_tau or cpu_time >= settings.max_cpu_time or gap <= settings.gap_relative or (ub - lb) <= settings.gap_absolute): stop = True # -- end main loop # Print last solution and exit util.print_LP_solution(settings, n1, n2, k, master, 'Final solution of the master:', output_stream = output_stream) total_time = time.clock() - start_time print(file = output_stream) print('Summary:', end = ' ', file = output_stream) print('ub {:.3f} lb {:.3f} '.format(ub, lb) + 'gap {:.2f} % tau {:d} '.format(100 * gap, tau) + 'time {:.4f}'.format(total_time), file = output_stream) output_stream.flush() return (master, ub, lb, gap, tau, total_time)
def pp_sddp(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, T_rows, D_rows, W_rows, b, d, w, scenarios, prob, z_lower, delta, output_stream=sys.stdout): """Apply the Pereira-Pinto SDDP algorithm for dynamic programming. Solve an infinite horizon stochastic dynamic program using the Pereira-Pinto SDDP algorithm, given the initial data. It is assumed that this is a minimization problem and the constraints are in >= form. The problem is written as: .. math :: \min \{\sum_{t=0}^\infty c^T x_t + h^T y_t : \forall t A x_t + G y_t \ge b + Ty_{t-1}, D x_t \ge d, W y_t \ge w\} Parameters ---------- settings : b2_sddp_settings.B2Settings Algorithmic settings. n1 : int Number of x variables of the problem. n2 : int Number of y variables of the problem. k : int Number of scenarios. m : int Number of rows in the first set of constraints A x + G y >= b + T y_{t-1}. p : int Number of rows in the second set of constraints D x >= d. q : int Number of rows in the third set of constraints W x >= w. x_obj : List[float] Cost coefficient for the x variables. y_obj : List[float] Cost coefficient for the y (state) variables. A_rows : List[List[int], List[float]] The coefficients of the rows of the A matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. G_rows : List[List[int], List[float]] The coefficients of the rows of the G matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. T_rows : List[List[int], List[float]] The coefficients of the rows of the T matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. D_rows : List[List[int], List[float]] The coefficients of the rows of the D matrix in D x >= d. These are in sparse form: indices and elements. W_rows : List[List[int], List[float]] The coefficients of the rows of the W matrix in W y >= w. These are in sparse form: indices and elements. b : List[float] The rhs of the first set of constraints in the master. d : List[float] The rhs of the second set of constraints in the master. w : List[float] The rhs of the third set of constraints in the master. scenarios : List[List[float]] The rhs vector (b, d, w) for each scenario. prob : List[float] The probability of each scenario. z_lower : List[float] Lower bounds on the objective value of each scenario. delta : float Discount factor, 0 <= delta < 1. output_stream : file Output stream. Must have a 'write' and 'flush' method. Returns ------- (cplex.Cplex, float, float, float, int, float) The master problem after convergence, the best upper bound, the best lower bound, the final gap (as a fraction, i.e. not in percentage point), the final length of the time horizon tau, the total CPU time in seconds. """ assert (isinstance(settings, B2Settings)) assert (len(x_obj) == n1) assert (len(y_obj) == n2) assert (len(A_rows) == m) assert (len(G_rows) == m) assert (len(T_rows) == m) assert (len(D_rows) == p) assert (len(W_rows) == q) assert (len(b) == m) assert (len(d) == p) assert (len(w) == q) assert (len(scenarios) == k) for i in range(k): assert (len(scenarios[i]) == m + p + q) assert (len(prob) == k) assert (len(z_lower) == k) assert (0 <= delta < 1) # Start counting time start_time = time.clock() # Initialize inverse CDF of the probability distribution of the # scenarios inverse_cdf = util.InverseCDF(prob) # Compute column representation of T T_cols = util.row_matrix_to_col_matrix(m, n2, T_rows) # Create the master and slave master_orig = create_master_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, D_rows, W_rows, b, d, w, scenarios, prob, z_lower, delta) slave_orig = create_slave_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, D_rows, W_rows, d, w, prob, z_lower, delta) # Store solutions for all forward passes fwpass_sol = list() # Store cost of primal solutions, i.e. sample paths cost_sol = list() # The best primal bound available so far best_ub = cplex.infinity # Is the stopping criterion satisfied? stop = False # Create master problem with fixed y variables pb_prob = pb.create_p_fixed_y_problem(settings, n1, n2, k, m, p, q, x_obj, y_obj, A_rows, G_rows, T_rows, D_rows, W_rows, d, w, scenarios) if (not settings.print_cplex_output): pb_prob.set_results_stream(None) else: pb_prob.set_results_stream(output_stream) # Compute cost of a single stage, using a fixed y policy ub_tail = pb.cost_tail_fixed_y(settings, n1, n2, k, m, p, q, T_cols, [0] * n2, 0, delta, scenarios, prob, 0, pb_prob) # Fraction of the absolute gap used to upper bound the error in # the infinite tail gap_frac = 0.9 # Length of the time horizon tau = get_time_horizon_length(delta, ub_tail, settings.gap_absolute * gap_frac) print('Pereira-Pinto time horizon of length {:d}'.format(tau), file=output_stream) output_stream.flush() # Make copies of master and slave master = [cplex.Cplex(master_orig) for t in range(tau)] slave = [cplex.Cplex(slave_orig) for t in range(tau)] # Cut activity levels cut_activity = [list() for t in range(tau)] for t in range(tau): if (not settings.print_cplex_output): master[t].set_results_stream(None) slave[t].set_results_stream(None) else: master[t].set_results_stream(output_stream) slave[t].set_results_stream(output_stream) # Start the PP SDDP algorithm! while (not stop): print('*** Major iteration after {:d} passes'.format(len(fwpass_sol)), file=output_stream) output_stream.flush() for num_pass in range(settings.num_passes): # Collect forward solution x, y, z = single_forward_step(settings, n1, n2, k, m, p, q, T_cols, [0] * n2, scenarios, prob, inverse_cdf, master[0], cut_activity[0]) sol_x, sol_y, sol_z = [x], [y], [z] for t in range(1, tau): # One step at a time x, y, z = single_forward_step(settings, n1, n2, k, m, p, q, T_cols, sol_y[-1], scenarios, prob, inverse_cdf, master[t], cut_activity[t]) sol_x.append(x) sol_y.append(y) sol_z.append(z) # Store all LP solutions fwpass_sol.append((sol_x, sol_y, sol_z)) # Computation of upper bounds cost_sol.append( pb.cost_primal_sol(sol_x, sol_y, x_obj, y_obj, tau, delta)) # Backward pass: collect Benders cuts for t in reversed(range(tau)): cuts = backward_pass(settings, n1, n2, k, m, p, q, 1, T_cols, scenarios, prob, [sol_x[t]], [sol_y[t]], [sol_z[t]], slave[t], num_pass) util.add_cuts(master[t], cuts) # -- end passes # Clean up LPs for t in range(1, tau): cut_activity[t] = util.purge_cuts(settings, m, p, q, cut_activity[t], master[t], slave[t]) # Primal bound: compute cost based on samples ub = pb.primal_bound(settings, cost_sol) # Get current dual bound and compute optimality gap master[0].linear_constraints.set_rhs([(i, b[i]) for i in range(m)]) master[0].solve() lb = master[0].solution.get_objective_value() gap = ((ub + settings.gap_absolute * gap_frac - lb) / (ub + 1.0e-10)) # Report progress status print('Primal bound: {:f} '.format(ub) + 'Dual bound: {:f} '.format(lb) + 'gap {:.2f} %'.format(100 * gap), file=output_stream) output_stream.flush() cpu_time = time.clock() - start_time # Stop when the optimality gap is small enough if (tau >= settings.max_tau or cpu_time >= settings.max_cpu_time or gap <= settings.gap_relative or (ub - lb) <= settings.gap_absolute * (1 - gap_frac)): stop = True # -- end main loop # Print last solution and exit util.print_LP_solution(settings, n1, n2, k, master[0], 'Final solution of the master:', output_stream=output_stream) total_time = time.clock() - start_time print(file=output_stream) print('Summary:', end=' ', file=output_stream) print('ub {:.3f} lb {:.3f} '.format(ub + settings.gap_absolute * gap_frac, lb) + 'gap {:.2f} % tau {:d} '.format(100 * gap, tau) + 'time {:.4f}'.format(total_time), file=output_stream) output_stream.flush() return (master[0], ub + settings.gap_absolute * gap_frac, lb, gap, tau, total_time)
def forward_pass(settings, n1, n2, k, m, p, q, tau, T_cols, b, d, w, scenarios, prob, inverse_cdf, master, current_pass, cut_activity): """Perform the forward pass of the B^2 SDDP algorithm. Generate sample path and compute the corresponding LP solutions moving forward in time. LP solutions could be aggregated over several samples, but at the moment we do not do it. Parameters ---------- settings : b2_sddp_settings.B2Settings Algorithmic settings. n1 : int Number of x variables of the problem. n2 : int Number of y variables of the problem. k : int Number of scenarios. m : int Number of rows in the first set of constraints A x + G y >= b + T y_{t-1}. p : int Number of rows in the second set of constraints D x >= d. q : int Number of rows in the third set of constraints W x >= w. tau : int The current length of the time horizon. T_cols : List[List[int], List[float]] The coefficients of the columns of the T matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. b : List[float] The rhs of the master. d : List[float] The rhs of the second set of constraints in the master. w : List[float] The rhs of the third set of constraints in the master. scenarios : List[List[float]] The rhs vector (b, d, w) for each scenario. prob : List[float] The probability of each scenario. inverse_cdf : b2_sddp_utility.InverseCDF Inverse CDF of the probability distribution of the scenarios. master : cplex.Cplex Master problem. current_pass : int Identifier of the current pass. cut_activity : List[int] Number of consecutive rounds that a cut has been inactive. This will be updated at the end of the forward pass. Returns ------- (List[List[float]], List[List[float]], List[List[float]]) A triple containing the x, y, z components of the LP solutions. Each list will have length equal to tau, and contain the corresponding values of the solutions. """ assert (isinstance(settings, B2Settings)) assert (len(T_cols) == n2) assert (len(b) == m) assert (len(d) == p) assert (len(w) == q) assert (len(scenarios) == k) for i in range(k): assert (len(scenarios[i]) == m + p + q) assert (len(prob) == k) assert (isinstance(inverse_cdf, util.InverseCDF)) assert (current_pass < settings.num_passes) # Numbering system for LPs written to file, for debugging lp_id = 0 # Adjust length of cut activity vector num_rows = master.linear_constraints.get_num() if (len(cut_activity) < num_rows - (m + p + q)): cut_activity.extend([0] * (num_rows - (m + p + q) - len(cut_activity))) # Consider sample paths up to this stage sample_path = generate_sample_path(settings, tau, inverse_cdf) # Store solutions in these lists x = list() y = list() z = list() for t in range(tau): # Find current scenario index j = sample_path[t] if (t == 0): # The rhs value is the initial one, b master.linear_constraints.set_rhs([(i, b[i]) for i in range(m)]) master.linear_constraints.set_rhs([(m + i, d[i]) for i in range(p)]) master.linear_constraints.set_rhs([(m + p + i, w[i]) for i in range(q)]) else: # Get rhs from the corresponding scenario, adding carried # over resources carry_over = util.col_matrix_vector_product(m, n2, T_cols, y[-1]) rhs = ([scenarios[j][i] + carry_over[i] for i in range(m)] + scenarios[j][m:]) master.linear_constraints.set_rhs([(i, rhs[i]) for i in range(m + p + q)]) # Save problem for debugging if (settings.debug_save_lp): print('Writing problem master_' + str(tau) + '_' + str(current_pass) + '_' + str(lp_id) + '.lp') master.write( 'master_' + str(tau) + '_' + str(current_pass) + '_' + str(lp_id) + '.lp', 'lp') lp_id += 1 master.solve() if (util.is_problem_optimal(settings, master)): # Save all solutions x.append(master.solution.get_values(0, n1 - 1)) y.append(master.solution.get_values(n1, n1 + n2 - 1)) z.append(master.solution.get_values(n1 + n2, n1 + n2 + k - 1)) # Check cut activity dual = master.solution.get_dual_values(m + p + q, num_rows - 1) for (i, val) in enumerate(dual): if (abs(val) <= settings.eps_activity): cut_activity[i] += 1 else: cut_activity[i] = 0 if (settings.print_lp_solution_forward): util.print_LP_solution( settings, n1, n2, k, master, 'FORWARD At stage ' + '{:d}, '.format(t) + 'scenario {:d}'.format(j)) else: print('Cannot solve master problem; abort.') sys.exit() return (x, y, z)
def backward_pass(settings, n1, n2, k, m, p, q, tau, T_cols, scenarios, prob, x, y, z, slave, current_pass): """Perform the backward pass of the B^2 SDDP algorithm. Generate Benders cuts moving backward in time using the given sequence of LP solutions. Cuts can be aggregated depending on the settings. They are automatically added to the slave problem. Parameters ---------- settings : b2_sddp_settings.B2Settings Algorithmic settings. n1 : int Number of x variables of the problem. n2 : int Number of y variables of the problem. k : int Number of scenarios. m : int Number of rows in the first set of constraints A x + G y >= b + T y_{t-1}. p : int Number of rows in the second set of constraints D x >= d. q : int Number of rows in the third set of constraints W x >= w. tau : int The current length of the time horizon. scenarios : List[List[float]] The rhs vector (b, d, w) for each scenario. prob : List[float] The probability of each scenario. x : List[List[float]] The values of the x component of the solution in the forward pass. y : List[List[float]] The values of the y component of the solution in the forward pass. z : List[List[float]] The values of the z component of the solution in the forward pass. slave : cplex.Cplex Cut generating problem (i.e. slave). current_pass : int Identifier of the current pass. Returns ------- (List[b2_sddp_utility.CutData]) A list of generated cuts, that should be added to the master. """ assert (isinstance(settings, B2Settings)) assert (len(T_cols) == n2) assert (len(scenarios) == k) for i in range(k): assert (len(scenarios[i]) == m + p + q) assert (len(prob) == k) assert (len(x) == tau) assert (len(y) == tau) assert (len(z) == tau) assert (current_pass < settings.num_passes) # Numbering system for LPs written to file, for debugging lp_id = 0 # Store cuts for the master here pool = list() # Backward pass: collect Benders cuts for t in reversed(range(1, tau + 1)): # Pool of cuts from the current pass local_pool = list() # Scenario a cut was generated from local_pool_scenario = list() for j in range(k): # Construct rhs of the slave, remembering that we have the # optimality cut together with the resource constraints carry_over = util.col_matrix_vector_product( m, n2, T_cols, y[t - 1]) rhs = ([scenarios[j][i] + carry_over[i] for i in range(m)] + scenarios[j][m:]) slave.linear_constraints.set_rhs([(i, rhs[i]) for i in range(m + p + q)]) slave.linear_constraints.set_rhs(m + p + q, -z[t - 1][j]) m_with_cuts = slave.linear_constraints.get_num() rhs_with_cuts = slave.linear_constraints.get_rhs() # Save problem for debugging if (settings.debug_save_lp): print('Writing problem slave_' + str(tau) + '_' + str(current_pass) + '_' + str(lp_id) + '.lp') slave.write( 'slave_' + str(tau) + '_' + str(current_pass) + '_' + str(lp_id) + '.lp', 'lp') lp_id += 1 slave.solve() if (util.is_problem_optimal(settings, slave) and slave.solution.get_objective_value() >= settings.eps_opt): # The problem is infeasible; collect a cut in >= form. dual = slave.solution.get_dual_values() # The cut rhs depends on the scenario's rhs, and on # the part that comes from the Benders cuts. cut_rhs = sum(dual[i] * scenarios[j][i] for i in range(m + p + q)) cut_rhs += sum(dual[i] * rhs_with_cuts[i] for i in range(m + p + q + 1, m_with_cuts)) cut_coeff = util.vector_col_matrix_product( m, n2, dual[:m], T_cols) # We usually normalize by the dual variable that # multiplies the z variable for the current scenario, # but if such variable is zero, we simply do not # normalize the cut. if (abs(dual[m + p + q]) > settings.eps_zero): cut_rhs /= dual[m + p + q] # Now generate the cut coefficients cut_ind = [i for i in range(n1, n1 + n2)] + [n1 + n2 + j] cut_elem = ( [-cut_coeff[i] / dual[m + p + q] for i in range(n2)] + [1]) else: cut_ind = [i for i in range(n1, n1 + n2)] cut_elem = [-cut_coeff[i] for i in range(n2)] cut = util.CutData(cut_ind, cut_elem, 'G', cut_rhs, 'c_' + str(t) + '_' + str(j)) local_pool.append(cut) local_pool_scenario.append(j) # Check violation if (settings.debug_check_violation): primal_sol = x[t - 1] + y[t - 1] + z[t - 1] lhs = sum(primal_sol[cut_ind[i]] * cut_elem[i] for i in range(len(cut_ind))) if (lhs - cut_rhs >= 0): print('Cut not violated') print('Cut {:f} >= {:f}'.format(lhs, cut_rhs)) print(local_pool[-1]) sys.exit() if (settings.print_lp_solution_backward and util.is_problem_optimal(settings, slave)): util.print_LP_solution( settings, n1, n2, k, slave, 'BACKWARD At stage ' + '{:d}, scenario {:d}'.format(t, j), True) # Add cut to the slave if (settings.add_cuts_immediately and not settings.aggregate_cuts): add_master_cut_to_slave(settings, n1, n2, k, cut, slave) if (settings.aggregate_cuts): # Define the aggregate cut as the surrogate of all the cuts if (local_pool): aggr_cut = generate_aggregate_cut(settings, n1, n2, k, prob, local_pool, local_pool_scenario, 'c_aggr_' + str(t)) pool.append(aggr_cut) add_master_cut_to_slave(settings, n1, n2, k, aggr_cut, slave) else: # If the cut was not already added, we should add it now if (not settings.add_cuts_immediately): for cut in local_pool: add_master_cut_to_slave(settings, n1, n2, k, cut, slave) pool.extend(local_pool) return pool
def single_forward_step(settings, n1, n2, k, m, p, q, T_cols, carry, scenarios, prob, inverse_cdf, master, cut_activity): """Perform a single forward step of the B^2 SDDP algorithm. Sample a scenario and compute the corresponding LP solution. Parameters ---------- settings : b2_sddp_settings.B2Settings Algorithmic settings. n1 : int Number of x variables of the problem. n2 : int Number of y variables of the problem. k : int Number of scenarios. m : int Number of rows in the first set of constraints A x + G y >= b + T y_{t-1}. p : int Number of rows in the second set of constraints D x >= d. q : int Number of rows in the third set of constraints W x >= w. T_cols : List[List[int], List[float]] The coefficients of the columns of the T matrix in A x + G y >= b + T y_{t-1}. These are in sparse form: indices and elements. carry : List[float] Carried over inventory from previous time period, i.e. y_{t-1}. scenarios : List[List[float]] The rhs vector (b, d, w) for each scenario. prob : List[float] The probability of each scenario. inverse_cdf : b2_sddp_utility.InverseCDF Inverse CDF of the probability distribution of the scenarios. master : cplex.Cplex Master problem. cut_activity : List[int] Number of consecutive rounds that a cut has been inactive. This will be updated at the end of the forward pass. Returns ------- (List[float], List[float], List[float]) A triple containing the x, y, z components of the LP solutions. """ assert (isinstance(settings, B2Settings)) assert (len(T_cols) == n2) assert (len(carry) == n2) assert (len(scenarios) == k) for i in range(k): assert (len(scenarios[i]) == m + p + q) assert (len(prob) == k) assert (isinstance(inverse_cdf, util.InverseCDF)) # Adjust length of cut activity vector num_rows = master.linear_constraints.get_num() if (len(cut_activity) < num_rows - (m + p + q)): cut_activity.extend([0] * (num_rows - (m + p + q) - len(cut_activity))) # Sample current scenario index j = inverse_cdf.at(random.uniform(0, 1)) # Get rhs from the corresponding scenario, adding # carried over resources carry_over = util.col_matrix_vector_product(m, n2, T_cols, carry) rhs = ([scenarios[j][i] + carry_over[i] for i in range(m)] + scenarios[j][m:]) master.linear_constraints.set_rhs([(i, rhs[i]) for i in range(m + p + q)]) if (settings.debug_save_lp): print('Writing problem master_single_fp.lp') master.write('master_single_fp.lp', 'lp') master.solve() if (util.is_problem_optimal(settings, master)): # Save all solutions x = master.solution.get_values(0, n1 - 1) y = master.solution.get_values(n1, n1 + n2 - 1) z = master.solution.get_values(n1 + n2, n1 + n2 + k - 1) # Check cut activity dual = master.solution.get_dual_values(m + p + q, num_rows - 1) for (i, val) in enumerate(dual): if (abs(val) <= settings.eps_activity): cut_activity[i] += 1 else: cut_activity[i] = 0 # Report progress status if (settings.print_lp_solution_forward): util.print_LP_solution( settings, n1, n2, k, master, 'FORWARD At stage ' + '{:d}, '.format(t) + 'scenario {:d}'.format(j)) else: print('Cannot solve master problem; abort.') sys.exit() return (x, y, z)