From 00d7a5fd229d0d2c4ba7a1e834187daabefb7b8e Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 15:11:04 -0400 Subject: [PATCH 01/30] Redefining typedefs using Abstract Types --- src/stopping.jl | 8 -- src/typedefs.jl | 323 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 287 insertions(+), 44 deletions(-) diff --git a/src/stopping.jl b/src/stopping.jl index cd5ddca3..e1cc858c 100644 --- a/src/stopping.jl +++ b/src/stopping.jl @@ -14,14 +14,6 @@ # ======================= Deterministic Stopping Rule ====================== # -""" - DeterministicStopping() - -Terminate the algorithm once optimality is reached. -""" -mutable struct DeterministicStopping <: SDDP.AbstractStoppingRule -end - stopping_rule_status(::DeterministicStopping) = :DeterministicStopping function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::DeterministicStopping, loop::Symbol) diff --git a/src/typedefs.jl b/src/typedefs.jl index 2bba11a2..0ce91614 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -18,45 +18,296 @@ import JuMP import Revise -# Mutable struct for bundle parameters if level bundle method is used -mutable struct BundleParams - bundle_alpha :: Float64 - bundle_factor :: Float64 - level_factor :: Float64 +# Deterministic stopping rule +################################################################################ +""" + DeterministicStopping() -# Mutable struct for algorithmic parameters -mutable struct AlgoParams - # optimality tolerances for the MILP - ############################################################################ - opt_rtol :: Float64 - opt_atol :: Float64 - # binary approximation parameters - ############################################################################ - binary_approx :: Bool - #NOTE: so far, binary precision is the same for all stages - binary_precision :: Dict{Symbol, Float64} - # regularization parameters - ############################################################################ - regularization :: Bool +Terminate the algorithm once optimality is reached. +""" +mutable struct DeterministicStopping <: SDDP.AbstractStoppingRule + rtol::Float64 + atol::Float64 + function DeterministicStopping(; + rtol=1e-8, + atol=1e-8 + ) + return new(rtol, atol) + end +end + +# Different regimes for state approximation +################################################################################ +abstract type AbstractStateApproximationRegime end + +mutable struct BinaryApproximation <: AbstractStateApproximationRegime + binary_precision::Dict{Symbol, Float64} # so far, binary precision is the same for all stages + function BinaryApproximation(; + binary_precision = ..., + ) + return new(binary_precision) + end +end + +mutable struct NoStateApproximation <: AbstractStateApproximationRegime end + +""" +BinaryApproximation means that a dynamically refined binary approximation + is used in the backward pass. It also implies that cuts have to be projected + back to the original space. +NoStateApproximation means that all cuts are + generated in the original space, and thus may not be tight. +Default is BinaryApproximation. +""" + +# Different regimes for regularization +################################################################################ +abstract type AbstractRegularizationRegime end + +mutable struct Regularization <: AbstractRegularizationRegime sigma :: Vector{Float64} sigma_factor :: Float64 - # lagrangian dual specific parameters - ############################################################################ - lagrangian_atol :: Float64 - lagrangian_rtol :: Float64 - lagrangian_iteration_limit :: Int - dual_initialization_method :: Symbol - dual_solution_method :: Symbol - dual_status_regime :: Symbol - magnanti_wong :: Bool - bundle_params :: Union{Nothing,DynamicSDDiP.BundleParams} - # nonlinear cut parameters - ############################################################################ - cut_projection_method :: Symbol - cut_selection :: Bool - # debugging parameter - ############################################################################ - infiltrate_state :: Symbol + function BinaryApproximation(; + sigma = 1, + sigma_factor = 5 + ) + return new(sigma, sigma_factor) + end +end + +mutable struct NoRegularization <: AbstractRegularizationRegime end + +""" +Regularization means that in the forward pass some regularized value functions + are considered. It also implies that a sigma test is conducted once the + stopping criterion is satisfied. Furthermore, it can be exploited in combination + with bounding the dual variables. +NoRegularization means that no regularization is used. This may be detrimental + w.r.t. convergence. +Default is Regularization. +""" + +# Different regimes for initialization of dual multipliers +################################################################################ +abstract type AbstractDualInitializationRegime end + +mutable struct ZeroDuals <: AbstractDualInitializationRegime end +mutable struct LPDuals <: AbstractDualInitializationRegime end +mutable struct CPLEXFixed <: AbstractDualInitializationRegime end + +""" +ZeroDuals means that the dual multipliers are initialized as zero. +LPDuals means that the LP relaxation is solved and the corresponding optimal + dual multipliers are used as an initial solution. +CPLEXFixed can only be chosen if CPLEX is used to solve the subproblems. + In contrast to Gurobi, CPLEX provides marginal values even when the primal + MILP is solved. These dual values are determined by solving an LP after fixing + the integer variables to their optimal values. They are hard to interpret, + but still may be used for initialization. +Default is ZeroDuals. +""" + +# Different regimes for solving the Lagrangian dual +################################################################################ +abstract type AbstractDualSolutionRegime end + +mutable struct Kelley <: AbstractDualSolutionRegime + atol::Float64 + rtol::Float64 + iteration_limit::Int + function LevelBundle(; + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, + ) + return new(atol, rtol, iteration_limit) + end +end + +mutable struct LevelBundle <: AbstractDualSolutionRegime + atol::Float64 + rtol::Float64 + iteration_limit::Int + bundle_alpha::Float64 + bundle_factor::Float64 + level_factor::Float64 + function LevelBundle(; + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, + bundle_alpha = 1.0, + bundle_factor = 1.0, + level_factor = 1.0, + ) + return new(atol, rtol, iteration_limit, bundle_alpha, bundle_factor, level_factor) + end +end + +""" +Kelley means that a classical cutting-plane method is used to solve the dual + problem. +LevelBundle means that a level bundle method with specified parameters is + used to solve the dual problem. +Default is Kelley. +""" + +# Different regimes for bounding the dual variables in the Lagrangian dual +################################################################################ +abstract type AbstractDualBoundRegime end + +mutable struct ValueBound <: AbstractDualBoundRegime end +mutable struct NormBound <: AbstractDualBoundRegime end +mutable struct BothBounds <: AbstractDualBoundRegime end + +""" +ValueBound means that the optimal value of the Lagrangian dual is bounded from + the beginning using the optimal value of the primal problem (as in the + SDDP package). However, the dual multipliers are not bounded. + This may result in infinitely steep cuts. +NormBound means that the dual multipliers in the Lagrangian dual are bounded + in their norm. This makes most sense when using a regularization and choosing + a dual bound related to the regularization parameter sigma. +BothBounds means that ValueBound and NormBound are both used. +Default is BothBounds. +""" + +# Different regimes for handling the precision of the Lagrangian dual solution +################################################################################ +abstract type AbstractDualStatusRegime end + +mutable struct Rigorous <: AbstractDualStatusRegime end +mutable struct Lax <: AbstractDualStatusRegime end + +""" +Rigorous means that the Lagrangian cuts are only used if the Lagrangian + duals are solved as attempted (i.e. to approximate optimality). Otherwise, + the algorithm terminates with an error. +Lax means that even if the Lagrangian dual is not solved as attempted + (e.g. due to reaching the iteration limit without convergence, + due to subgradients being zero despite LB!=UB, due to numerical issues, + due to bound stalling), the current values of the multipliers are used + to define a valid cut for the value function, hoping that this cut + will suffice to improve the current incumbent. +This only makes a difference if the cut type is LagrangianCut. +Default is Rigorous. +""" + +# Different regimes for choosing the best dual solution if degenerated +################################################################################ +abstract type AbstractDualChoiceRegime end + +mutable struct StandardChoice <: AbstractDualChoiceRegime end +mutable struct MagnantiWongChoice <: AbstractDualChoiceRegime end + +""" +Standard choice means that the Lagrangian multipliers are used as determined + by the cutting-plane or level bundle method. +MagnantiWongChoice means that it is attempted to determine pareto-optimal + dual multipliers (as in the newer SDDP package version). +Default is MagnantiWongChoice. +""" + +# Different regimes for the cut type +################################################################################ +abstract type AbstractCutTypeRegime end + +mutable struct LagrangianCut <: AbstractCutTypeRegime end +mutable struct BendersCut <: AbstractCutTypeRegime end +mutable struct StrengthenedCut <: AbstractCutTypeRegime end + +""" +LagrangianCut means that the Lagrangian dual is (approximately) solved to obtain + a cut. +BendersCut means that the LP relaxation is solved to obtain a cut. +StrengthenedCut means that the Lagrangian relaxation is solved using the optimal + dual multiplier of the LP relaxation to obtain a strengthened Benders cuts. +Default is LagrangianCut. +""" + +# Different regimes for the cut projection +################################################################################ +abstract type AbstractCutProjectionRegime end + +mutable struct BigM <: AbstractCutProjectionRegime end +mutable struct SOS1 <: AbstractCutProjectionRegime end +mutable struct Bilinear <: AbstractCutProjectionRegime end + +""" +BigM means that the complementarity constraints of the KKT conditions of + the cut projection closure are reformulated using a big-M approach. +SOS1 means that the complemnentarity constraints of the KKT conditions of + the cut projection closure are reformulated using SOS1 constraints. +Bilinear means that by using strong duality the cuts are integrated in a + bilinear way. This implies that MINLP solvers have to be used to solve + the subproblems. +Default is BigM. +Note that this only makes a differences if a binary approximation is used, + otherwise no projection is required at all. +""" + +# Different regimes for the cut selection +################################################################################ +abstract type AbstractCutSelectionRegime end + +mutable struct CutSelection <: AbstractCutSelectionRegime end +mutable struct NoCutSelection <: AbstractCutSelectionRegime end + +""" +CutSelection means that a simple cut selection procedure is used to identify + dominated cuts (as in SDDP). +NoCutSelection means that no such procedure is used, so all cuts are used + in each iteration. +""" + +################################################################################ +################################################################################ + +# Mutable struct for algorithmic parameters +mutable struct AlgoParams + stopping_rules::Vector{SDDP.AbstractStoppingRule} + state_approximation_regime::AbstractStateApproximationRegime + regularization_regime::AbstractRegularizationRegime + dual_initialization_regime::AbstractDualInitializationRegime + dual_solution_regime::AbstractDualSolutionRegime + dual_bound_regime::AbstractDualBoundRegime + dual_status_regime::AbstractDualStatusRegime + dual_choice_regime::AbstractDualChoiceRegime + cut_type_regime::AbstractCutTypeRegime + cut_projection_regime::AbstractCutProjectionRegime + cut_selection_regime::AbstractCutSelectionRegime + infiltrate_state::Symbol + + function AlgoParams(; + stopping_rules = [DeterministicStopping()], + state_approximation_regime = BinaryApproximation(), + regularization_regime = Regularization(), + dual_initialization_regime = ZeroDuals(), + dual_solution_regime = Kelley(), + dual_bound_regime = BothBounds(), + dual_status_regime = Rigorous(), + dual_choice_regime = MagnantiWongChoice(), + cut_type_regime = LagrangianCut(), + cut_projection_regime = BigM(), + cut_selection_regime = CutSelection(), + infiltrate_state = :None + ) + return new( + stopping_rules, + state_approximation_regime, + regularization_regime, + dual_initialization_regime, + dual_solution_regime, + dual_bound_regime, + dual_status_regime, + dual_choice_regime, + cut_type_regime, + cut_projection_regime, + cut_selection_regime, + infiltrate_state, + ) + end + + end # struct for solvers to be used From 484cb603755968a98d8cf25df9190571f2feba9a Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 15:23:05 -0400 Subject: [PATCH 02/30] Redefined typedefs once again, such that we have a hierarchy of parameters and parameters are only required to be defined in a setting where they actually make sense. --- src/typedefs.jl | 251 +++++++++++++++++++++++++----------------------- 1 file changed, 133 insertions(+), 118 deletions(-) diff --git a/src/typedefs.jl b/src/typedefs.jl index 0ce91614..6778273c 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -18,7 +18,8 @@ import JuMP import Revise -# Deterministic stopping rule +################################################################################ +# STOPPING RULES ################################################################################ """ DeterministicStopping() @@ -36,65 +37,11 @@ mutable struct DeterministicStopping <: SDDP.AbstractStoppingRule end end -# Different regimes for state approximation ################################################################################ -abstract type AbstractStateApproximationRegime end - -mutable struct BinaryApproximation <: AbstractStateApproximationRegime - binary_precision::Dict{Symbol, Float64} # so far, binary precision is the same for all stages - function BinaryApproximation(; - binary_precision = ..., - ) - return new(binary_precision) - end -end - -mutable struct NoStateApproximation <: AbstractStateApproximationRegime end - -""" -BinaryApproximation means that a dynamically refined binary approximation - is used in the backward pass. It also implies that cuts have to be projected - back to the original space. -NoStateApproximation means that all cuts are - generated in the original space, and thus may not be tight. -Default is BinaryApproximation. -""" - -# Different regimes for regularization -################################################################################ -abstract type AbstractRegularizationRegime end - -mutable struct Regularization <: AbstractRegularizationRegime - sigma :: Vector{Float64} - sigma_factor :: Float64 - function BinaryApproximation(; - sigma = 1, - sigma_factor = 5 - ) - return new(sigma, sigma_factor) - end -end - -mutable struct NoRegularization <: AbstractRegularizationRegime end - -""" -Regularization means that in the forward pass some regularized value functions - are considered. It also implies that a sigma test is conducted once the - stopping criterion is satisfied. Furthermore, it can be exploited in combination - with bounding the dual variables. -NoRegularization means that no regularization is used. This may be detrimental - w.r.t. convergence. -Default is Regularization. -""" - -# Different regimes for initialization of dual multipliers +# INITIALIZIING DUAL MULTIPLIERS ################################################################################ abstract type AbstractDualInitializationRegime end -mutable struct ZeroDuals <: AbstractDualInitializationRegime end -mutable struct LPDuals <: AbstractDualInitializationRegime end -mutable struct CPLEXFixed <: AbstractDualInitializationRegime end - """ ZeroDuals means that the dual multipliers are initialized as zero. LPDuals means that the LP relaxation is solved and the corresponding optimal @@ -107,10 +54,23 @@ CPLEXFixed can only be chosen if CPLEX is used to solve the subproblems. Default is ZeroDuals. """ -# Different regimes for solving the Lagrangian dual +mutable struct ZeroDuals <: AbstractDualInitializationRegime end +mutable struct LPDuals <: AbstractDualInitializationRegime end +mutable struct CPLEXFixed <: AbstractDualInitializationRegime end + +################################################################################ +# SOLUTION METHOD FOR LAGRANGIAN DUAL ################################################################################ abstract type AbstractDualSolutionRegime end +""" +Kelley means that a classical cutting-plane method is used to solve the dual + problem. +LevelBundle means that a level bundle method with specified parameters is + used to solve the dual problem. +Default is Kelley. +""" + mutable struct Kelley <: AbstractDualSolutionRegime atol::Float64 rtol::Float64 @@ -143,22 +103,11 @@ mutable struct LevelBundle <: AbstractDualSolutionRegime end end -""" -Kelley means that a classical cutting-plane method is used to solve the dual - problem. -LevelBundle means that a level bundle method with specified parameters is - used to solve the dual problem. -Default is Kelley. -""" - -# Different regimes for bounding the dual variables in the Lagrangian dual +################################################################################ +# BOUNDS IN LAGRANGIAL DUAL ################################################################################ abstract type AbstractDualBoundRegime end -mutable struct ValueBound <: AbstractDualBoundRegime end -mutable struct NormBound <: AbstractDualBoundRegime end -mutable struct BothBounds <: AbstractDualBoundRegime end - """ ValueBound means that the optimal value of the Lagrangian dual is bounded from the beginning using the optimal value of the primal problem (as in the @@ -171,13 +120,15 @@ BothBounds means that ValueBound and NormBound are both used. Default is BothBounds. """ -# Different regimes for handling the precision of the Lagrangian dual solution +mutable struct ValueBound <: AbstractDualBoundRegime end +mutable struct NormBound <: AbstractDualBoundRegime end +mutable struct BothBounds <: AbstractDualBoundRegime end + +################################################################################ +# HOW TO HANDLE DUAL SOLUTIONS ################################################################################ abstract type AbstractDualStatusRegime end -mutable struct Rigorous <: AbstractDualStatusRegime end -mutable struct Lax <: AbstractDualStatusRegime end - """ Rigorous means that the Lagrangian cuts are only used if the Lagrangian duals are solved as attempted (i.e. to approximate optimality). Otherwise, @@ -192,13 +143,14 @@ This only makes a difference if the cut type is LagrangianCut. Default is Rigorous. """ -# Different regimes for choosing the best dual solution if degenerated +mutable struct Rigorous <: AbstractDualStatusRegime end +mutable struct Lax <: AbstractDualStatusRegime end + +################################################################################ +# HOW TO HANDLE DEGENERATE SOLUTIONS OF THE DUAL ################################################################################ abstract type AbstractDualChoiceRegime end -mutable struct StandardChoice <: AbstractDualChoiceRegime end -mutable struct MagnantiWongChoice <: AbstractDualChoiceRegime end - """ Standard choice means that the Lagrangian multipliers are used as determined by the cutting-plane or level bundle method. @@ -207,31 +159,14 @@ MagnantiWongChoice means that it is attempted to determine pareto-optimal Default is MagnantiWongChoice. """ -# Different regimes for the cut type -################################################################################ -abstract type AbstractCutTypeRegime end - -mutable struct LagrangianCut <: AbstractCutTypeRegime end -mutable struct BendersCut <: AbstractCutTypeRegime end -mutable struct StrengthenedCut <: AbstractCutTypeRegime end - -""" -LagrangianCut means that the Lagrangian dual is (approximately) solved to obtain - a cut. -BendersCut means that the LP relaxation is solved to obtain a cut. -StrengthenedCut means that the Lagrangian relaxation is solved using the optimal - dual multiplier of the LP relaxation to obtain a strengthened Benders cuts. -Default is LagrangianCut. -""" +mutable struct StandardChoice <: AbstractDualChoiceRegime end +mutable struct MagnantiWongChoice <: AbstractDualChoiceRegime end -# Different regimes for the cut projection +################################################################################ +# CUT PROJECTION APPROACH ################################################################################ abstract type AbstractCutProjectionRegime end -mutable struct BigM <: AbstractCutProjectionRegime end -mutable struct SOS1 <: AbstractCutProjectionRegime end -mutable struct Bilinear <: AbstractCutProjectionRegime end - """ BigM means that the complementarity constraints of the KKT conditions of the cut projection closure are reformulated using a big-M approach. @@ -245,7 +180,105 @@ Note that this only makes a differences if a binary approximation is used, otherwise no projection is required at all. """ -# Different regimes for the cut selection +mutable struct BigM <: AbstractCutProjectionRegime end +mutable struct SOS1 <: AbstractCutProjectionRegime end +mutable struct Bilinear <: AbstractCutProjectionRegime end + +################################################################################ +# BINARY APPROXIMATION +################################################################################ +abstract type AbstractStateApproximationRegime end + +""" +BinaryApproximation means that a dynamically refined binary approximation + is used in the backward pass. It also implies that cuts have to be projected + back to the original space. +NoStateApproximation means that all cuts are + generated in the original space, and thus may not be tight. +Default is BinaryApproximation. +""" + +mutable struct BinaryApproximation <: AbstractStateApproximationRegime + binary_precision::Dict{Symbol, Float64} # so far, binary precision is the same for all stages + cut_projection_regime::AbstractCutProjectionRegime + function BinaryApproximation(; + binary_precision = ..., + cut_projection_regime = BigM(), + ) + return new(binary_precision, cut_projection_regime) + end +end + +mutable struct NoStateApproximation <: AbstractStateApproximationRegime end + +################################################################################ +# REGULARIZATION +################################################################################ +abstract type AbstractRegularizationRegime end + +mutable struct Regularization <: AbstractRegularizationRegime + sigma :: Vector{Float64} + sigma_factor :: Float64 + function BinaryApproximation(; + sigma = 1, + sigma_factor = 5 + ) + return new(sigma, sigma_factor) + end +end + +mutable struct NoRegularization <: AbstractRegularizationRegime end + +""" +Regularization means that in the forward pass some regularized value functions + are considered. It also implies that a sigma test is conducted once the + stopping criterion is satisfied. Furthermore, it can be exploited in combination + with bounding the dual variables. +NoRegularization means that no regularization is used. This may be detrimental + w.r.t. convergence. +Default is Regularization. +""" + +################################################################################ +# CUT FAMILY TO BE USED +################################################################################ +abstract type AbstractCutTypeRegime end + +mutable struct LagrangianCut <: AbstractCutTypeRegime end + +mutable struct LagrangianCut <: AbstractCutTypeRegime + dual_initialization_regime::AbstractDualInitializationRegime + dual_bound_regime::AbstractDualBoundRegime + dual_solution_regime::AbstractDualSolutionRegime + dual_choice_regime::AbstractDualChoiceRegime + dual_status_regime::AbstractDualStatusRegime + function BinaryApproximation(; + dual_initialization_regime = ZeroDuals(), + dual_bound_regime = BothBounds(), + dual_solution_regime = Kelley(), + dual_choice_regime = MagnantiWongChoice(), + dual_status_regime = Rigorous(), + ) + return new(dual_initialization_regime, dual_bound_regime, + dual_solution_regime, dual_choice_regime, dual_status_regime) + end +end + +mutable struct BendersCut <: AbstractCutTypeRegime end +mutable struct StrengthenedCut <: AbstractCutTypeRegime end + +""" +LagrangianCut means that the Lagrangian dual is (approximately) solved to obtain + a cut. In this case, a lot of parameters have to be defined to configure + the solution of the Lagrangian dual. +BendersCut means that the LP relaxation is solved to obtain a cut. +StrengthenedCut means that the Lagrangian relaxation is solved using the optimal + dual multiplier of the LP relaxation to obtain a strengthened Benders cuts. +Default is LagrangianCut. +""" + +################################################################################ +# CUT SELECTION ################################################################################ abstract type AbstractCutSelectionRegime end @@ -267,13 +300,7 @@ mutable struct AlgoParams stopping_rules::Vector{SDDP.AbstractStoppingRule} state_approximation_regime::AbstractStateApproximationRegime regularization_regime::AbstractRegularizationRegime - dual_initialization_regime::AbstractDualInitializationRegime - dual_solution_regime::AbstractDualSolutionRegime - dual_bound_regime::AbstractDualBoundRegime - dual_status_regime::AbstractDualStatusRegime - dual_choice_regime::AbstractDualChoiceRegime cut_type_regime::AbstractCutTypeRegime - cut_projection_regime::AbstractCutProjectionRegime cut_selection_regime::AbstractCutSelectionRegime infiltrate_state::Symbol @@ -281,13 +308,7 @@ mutable struct AlgoParams stopping_rules = [DeterministicStopping()], state_approximation_regime = BinaryApproximation(), regularization_regime = Regularization(), - dual_initialization_regime = ZeroDuals(), - dual_solution_regime = Kelley(), - dual_bound_regime = BothBounds(), - dual_status_regime = Rigorous(), - dual_choice_regime = MagnantiWongChoice(), cut_type_regime = LagrangianCut(), - cut_projection_regime = BigM(), cut_selection_regime = CutSelection(), infiltrate_state = :None ) @@ -295,13 +316,7 @@ mutable struct AlgoParams stopping_rules, state_approximation_regime, regularization_regime, - dual_initialization_regime, - dual_solution_regime, - dual_bound_regime, - dual_status_regime, - dual_choice_regime, cut_type_regime, - cut_projection_regime, cut_selection_regime, infiltrate_state, ) From cd21d59c52c307b1fda436c9eb5f1a26f3144103 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 15:27:14 -0400 Subject: [PATCH 03/30] Deleted some examples which are no longer required --- examples/UnitCommitment/InstanceStarter_2.jl | 74 ---- examples/UnitCommitment/InstanceStarter_3.jl | 71 --- examples/UnitCommitment/UC_10_10.jl | 399 ----------------- examples/UnitCommitment/UC_10_3.jl | 392 ----------------- examples/UnitCommitment/UC_10_5.jl | 393 ----------------- examples/UnitCommitment/UC_24_3.jl | 434 ------------------- examples/UnitCommitment/UC_3_10.jl | 399 ----------------- examples/UnitCommitment/UC_3_5.jl | 394 ----------------- examples/UnitCommitment/UC_4_10.jl | 399 ----------------- examples/UnitCommitment/UC_4_5.jl | 394 ----------------- examples/UnitCommitment/UC_5_10.jl | 399 ----------------- examples/UnitCommitment/UC_5_5.jl | 394 ----------------- src/typedefs.jl | 5 - 13 files changed, 4147 deletions(-) delete mode 100644 examples/UnitCommitment/InstanceStarter_2.jl delete mode 100644 examples/UnitCommitment/InstanceStarter_3.jl delete mode 100644 examples/UnitCommitment/UC_10_10.jl delete mode 100644 examples/UnitCommitment/UC_10_3.jl delete mode 100644 examples/UnitCommitment/UC_10_5.jl delete mode 100644 examples/UnitCommitment/UC_24_3.jl delete mode 100644 examples/UnitCommitment/UC_3_10.jl delete mode 100644 examples/UnitCommitment/UC_3_5.jl delete mode 100644 examples/UnitCommitment/UC_4_10.jl delete mode 100644 examples/UnitCommitment/UC_4_5.jl delete mode 100644 examples/UnitCommitment/UC_5_10.jl delete mode 100644 examples/UnitCommitment/UC_5_5.jl diff --git a/examples/UnitCommitment/InstanceStarter_2.jl b/examples/UnitCommitment/InstanceStarter_2.jl deleted file mode 100644 index 98d385e2..00000000 --- a/examples/UnitCommitment/InstanceStarter_2.jl +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using GAMS -#using SCIP -using Infiltrator - -include("UC_2_2.jl") -include("UC_2_5.jl") -include("UC_2_10.jl") -include("UC_3_5.jl") -include("UC_3_10.jl") -include("UC_4_5.jl") -include("UC_4_10.jl") -include("UC_5_5.jl") -include("UC_5_10.jl") -include("UC_10_3.jl") -include("UC_10_5.jl") -include("UC_10_10.jl") - -include("UC_2_2_Batt.jl") -include("UC_2_5_Batt.jl") -include("UC_2_10_Batt.jl") -include("UC_3_10_Batt.jl") -include("UC_4_10_Batt.jl") -include("UC_5_5_Batt.jl") -include("UC_10_3_Batt.jl") - - -""" -Used to run different instances of the unit commitment problem (different number -of stages, generators and different paramters) after each other -""" -function start_instances() - - # INSTANCE DEFINITIONS - parameter_sets = [ - [UC_5_5, 1e-4, 1e-4, :none, :kelley, 0.0, 1e-2, 1e-2], - [UC_5_5, 1e-4, 1e-4, :none, :bundle_level, 0.2, 1e-2, 1e-2], - ] - - for parameter_set in parameter_sets - - module_name = parameter_set[1] - lagrangian_atol = parameter_set[2] - lagrangian_rtol = parameter_set[3] - dual_initialization_regime = parameter_set[4] - lagrangian_method = parameter_set[5] - level_factor = parameter_set[6] - rgap_outer_loop = parameter_set[7] - rgap_inner_loop = parameter_set[8] - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "Baron", "CPLEX"] - - module_name.unitCommitment_with_parameters( - lagrangian_atol=lagrangian_atol, lagrangian_rtol=lagrangian_rtol, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, level_factor=level_factor, - solvers=solvers, epsilon_outerLoop = rgap_outer_loop, - epsilon_innerLoop = rgap_inner_loop - ) - - end -end - -start_instances() diff --git a/examples/UnitCommitment/InstanceStarter_3.jl b/examples/UnitCommitment/InstanceStarter_3.jl deleted file mode 100644 index 76382e95..00000000 --- a/examples/UnitCommitment/InstanceStarter_3.jl +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using GAMS -#using SCIP -using Infiltrator - -include("UC_2_2.jl") -include("UC_2_5.jl") -include("UC_2_10.jl") -include("UC_3_5.jl") -include("UC_3_10.jl") -include("UC_4_5.jl") -include("UC_4_10.jl") -include("UC_5_5.jl") - -include("UC_2_2_Batt.jl") -include("UC_2_5_Batt.jl") -include("UC_2_10_Batt.jl") -include("UC_3_10_Batt.jl") -include("UC_4_10_Batt.jl") -include("UC_5_5_Batt.jl") -include("UC_10_3_Batt.jl") - - -""" -Used to run different instances of the unit commitment problem (different number -of stages, generators and different paramters) after each other -""" -function start_instances() - - # INSTANCE DEFINITIONS - parameter_sets = [ - [UC_4_10, 1e-4, 1e-4, :none, :bundle_level, 0.2, 1e-2, 1e-2], - [UC_10_5, 1e-4, 1e-4, :none, :kelley, 0.0, 1e-2, 1e-2], - [UC_10_5, 1e-4, 1e-4, :none, :bundle_level, 0.2, 1e-2, 1e-2], - ] - - for parameter_set in parameter_sets - - module_name = parameter_set[1] - lagrangian_atol = parameter_set[2] - lagrangian_rtol = parameter_set[3] - dual_initialization_regime = parameter_set[4] - lagrangian_method = parameter_set[5] - level_factor = parameter_set[6] - rgap_outer_loop = parameter_set[7] - rgap_inner_loop = parameter_set[8] - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "Baron", "CPLEX"] - - module_name.unitCommitment_with_parameters( - lagrangian_atol=lagrangian_atol, lagrangian_rtol=lagrangian_rtol, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, level_factor=level_factor, - solvers=solvers, epsilon_outerLoop = rgap_outer_loop, - epsilon_innerLoop = rgap_inner_loop - ) - - end -end - -start_instances() diff --git a/examples/UnitCommitment/UC_10_10.jl b/examples/UnitCommitment/UC_10_10.jl deleted file mode 100644 index 4d8ffb21..00000000 --- a/examples/UnitCommitment/UC_10_10.jl +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 10 stages and 10 generators -""" - -module UC_10_10 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_10_10() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_10_10_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_10_10() - - generators = [ - Generator(0, 0.0, 1.18, 0.32, 48.94, 0.0, 182.35, 18.0, 0.42, 0.33, -0.21, 1.0, 0.0), - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.05, 0.48, 42.79, 0.0, 171.69, 17.0, 0.21, 0.22, -0.14, 1.02, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 0.72, 1.9, 0.5, 64.06, 0.0, 289.59, 28.0, 0.52, 0.62, -0.5, 1.0, 0.0), - Generator(0, 0.0, 2.08, 0.62, 60.28, 0.0, 286.89, 28.0, 0.67, 0.5, -0.35, 0.95, 0.0), - Generator(1, 0.55, 2.11, 0.55, 66.08, 0.0, 329.89, 33.0, 0.64, 0.69, -0.37, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [8.53 8.02 7.36 7.31 7.44 8.02 9.58 11.7 13.68 14.28] - - num_of_stages = 10 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_10_3.jl b/examples/UnitCommitment/UC_10_3.jl deleted file mode 100644 index 40d3d8b8..00000000 --- a/examples/UnitCommitment/UC_10_3.jl +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 10 stages and 3 generators -""" - -module UC_10_3 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 10000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.226], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "Baron", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 10000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.226], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_10_3() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_10_3_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_10_3() - - generators = [ - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [3.06 2.91 2.71 2.7 2.73 2.91 3.38 4.01 4.6 4.78] - - num_of_stages = 10 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_10_5.jl b/examples/UnitCommitment/UC_10_5.jl deleted file mode 100644 index c46efede..00000000 --- a/examples/UnitCommitment/UC_10_5.jl +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 10 stages and 5 generators -""" - -module UC_10_5 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 10000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.238], [0.226], [0.204], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/15 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=10000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.119], [0.113], [0.102], [0.282], [0.323]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/15, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_10_5() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_10_5_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_10_5() - - generators = [ - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 2.5 - - demand = [4.27 4.01 3.69 3.66 3.72 4.01 4.79 5.85 6.84 7.14] - - num_of_stages = 10 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_24_3.jl b/examples/UnitCommitment/UC_24_3.jl deleted file mode 100644 index 0486f3e9..00000000 --- a/examples/UnitCommitment/UC_24_3.jl +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 24 stages and 3 generators -""" - -module UC_24_3 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - #fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 - v_a::Float64 - v_b::Float64 - v_c::Float64 - v_d::Float64 - v_e::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 10000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = Dict(:valve => [[1.0], [1.0], [1.0]], :emi => [[0.226], [0.564], [0.646]]) - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :bundle_level - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "LINDOGLOBAL", "LINDOGLOBAL", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 10000, - iteration_limit::Int=1000, - time_limit::Int = 7200, - sigma::Vector{Float64} = [0.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0, 2000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Dict{Symbol,Array{Array{Float64,1},1}} = Dict(:valve => [[1.0], [1.0], [1.0]], :emi => [[0.226], [0.564], [0.646]]), # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/15, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "LINDOGLOBAL", "LINDOGLOBAL", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_24_3() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_24_3_v_e.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_24_3() - - generators = [ - Generator(0, 0.0, 1.13, 0.48, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0, 6.16, 49.01, 15.12, 1.13, 5), - Generator(1, 2.2, 2.82, 0.85, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0, 0.22, 61.19, 30.33, 2.82, 5), - Generator(0, 0.0, 3.23, 0.84, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0, 0.28, 54.35, 30.58, 3.23, 5), - ] - - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = 0.85 * [3.06 2.91 2.71 2.7 2.73 2.91 3.38 4.01 4.6 4.78 4.81 4.84 4.89 4.44 4.57 4.6 4.58 4.47 4.32 4.36 4.5 4.27 3.93 3.61] - - num_of_stages = 24 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - #JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR FUEL COST CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, valve_aux[1:num_of_generators]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].v_c * commit[i].out + valve_aux[i] == fuel_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR EMISSION COST CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY FOR VALVE-POINT EFFECT - # ------------------------------------------------------------------ - nlf_valve_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_valve_eval = function nonl_function_eval(y::Float64) - return generators[i].v_a * y^2 + generators[i].v_b * y + generators[i].v_d * abs(sin(generators[i].v_e * (generators[i].pmin - y))) - end - - # user-defined function for expression building - nlf_valve_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].v_a) * $(y)^2 + $(generators[i].v_b) * $(y) + $(generators[i].v_d) * abs(sin($(generators[i].v_e) * ($(generators[i].pmin) - $(y))))) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_valve_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:valve_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:valve_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_valve_eval, nlf_valve_expr, aux, [gen.out], :noshift, :replace, generators[i].pmin, :valve) - push!(nonlinearFunctionList, nlf) - end - - # DEFINE NONLINEARITY FOR EMISSION COST - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace, generators[i].pmin, :emi) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_3_10.jl b/examples/UnitCommitment/UC_3_10.jl deleted file mode 100644 index 603bfd43..00000000 --- a/examples/UnitCommitment/UC_3_10.jl +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 3 stages and 10 generators -""" - -module UC_3_10 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_3_10() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_3_10_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_3_10() - - generators = [ - Generator(0, 0.0, 1.18, 0.32, 48.94, 0.0, 182.35, 18.0, 0.42, 0.33, -0.21, 1.0, 0.0), - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.05, 0.48, 42.79, 0.0, 171.69, 17.0, 0.21, 0.22, -0.14, 1.02, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 0.72, 1.9, 0.5, 64.06, 0.0, 289.59, 28.0, 0.52, 0.62, -0.5, 1.0, 0.0), - Generator(0, 0.0, 2.08, 0.62, 60.28, 0.0, 286.89, 28.0, 0.67, 0.5, -0.35, 0.95, 0.0), - Generator(1, 0.55, 2.11, 0.55, 66.08, 0.0, 329.89, 33.0, 0.64, 0.69, -0.37, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [8.53 8.02 7.36] - - num_of_stages = 3 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_3_5.jl b/examples/UnitCommitment/UC_3_5.jl deleted file mode 100644 index 601b06a7..00000000 --- a/examples/UnitCommitment/UC_3_5.jl +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 3 stages and 5 generators -""" - -module UC_3_5 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.238], [0.226], [0.204], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.238], [0.226], [0.204], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_3_5() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_3_5_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_3_5() - - generators = [ - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [4.27 4.01 3.69] - - num_of_stages = 3 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_4_10.jl b/examples/UnitCommitment/UC_4_10.jl deleted file mode 100644 index 0342723d..00000000 --- a/examples/UnitCommitment/UC_4_10.jl +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 4 stages and 10 generators -""" - -module UC_4_10 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 10000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.118], [0.119], [0.105], [0.113], [0.102], [0.19], [0.208], [0.211], [0.282], [0.232]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/15, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_4_10() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_4_10_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_4_10() - - generators = [ - Generator(0, 0.0, 1.18, 0.32, 48.94, 0.0, 182.35, 18.0, 0.42, 0.33, -0.21, 1.0, 0.0), - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.05, 0.48, 42.79, 0.0, 171.69, 17.0, 0.21, 0.22, -0.14, 1.02, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 0.72, 1.9, 0.5, 64.06, 0.0, 289.59, 28.0, 0.52, 0.62, -0.5, 1.0, 0.0), - Generator(0, 0.0, 2.08, 0.62, 60.28, 0.0, 286.89, 28.0, 0.67, 0.5, -0.35, 0.95, 0.0), - Generator(1, 0.55, 2.11, 0.55, 66.08, 0.0, 329.89, 33.0, 0.64, 0.69, -0.37, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [8.53 8.02 7.36 7.31] - - num_of_stages = 4 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_4_5.jl b/examples/UnitCommitment/UC_4_5.jl deleted file mode 100644 index 1ce6834d..00000000 --- a/examples/UnitCommitment/UC_4_5.jl +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 4 stages and 5 generators -""" - -module UC_4_5 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.238], [0.226], [0.204], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 10000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.238], [0.226], [0.204], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_4_5() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_4_5_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_4_5() - - generators = [ - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [4.27 4.01 3.69 3.66] - - num_of_stages = 4 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_5_10.jl b/examples/UnitCommitment/UC_5_10.jl deleted file mode 100644 index 8c2a8d8c..00000000 --- a/examples/UnitCommitment/UC_5_10.jl +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 5 stages and 10 generators -""" - -module UC_5_10 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 10000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.118], [0.119], [0.105], [0.113], [0.102], [0.19], [0.208], [0.211], [0.282], [0.232]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/15, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_5_10() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_5_10_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_5_10() - - generators = [ - Generator(0, 0.0, 1.18, 0.32, 48.94, 0.0, 182.35, 18.0, 0.42, 0.33, -0.21, 1.0, 0.0), - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.05, 0.48, 42.79, 0.0, 171.69, 17.0, 0.21, 0.22, -0.14, 1.02, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 0.72, 1.9, 0.5, 64.06, 0.0, 289.59, 28.0, 0.52, 0.62, -0.5, 1.0, 0.0), - Generator(0, 0.0, 2.08, 0.62, 60.28, 0.0, 286.89, 28.0, 0.67, 0.5, -0.35, 0.95, 0.0), - Generator(1, 0.55, 2.11, 0.55, 66.08, 0.0, 329.89, 33.0, 0.64, 0.69, -0.37, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 5 - - demand = [8.53 8.02 7.36 7.31 7.44] - - num_of_stages = 5 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_5_5.jl b/examples/UnitCommitment/UC_5_5.jl deleted file mode 100644 index f9ccbfab..00000000 --- a/examples/UnitCommitment/UC_5_5.jl +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 5 stages and 5 generators -""" - -module UC_5_5 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0, 1000.0, 1000.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.238], [0.226], [0.204], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0, 1000.0, 1000.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.238], [0.226], [0.204], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_5_5() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_5_5_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_5_5() - - generators = [ - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [4.27 4.01 3.69 3.66 3.72] - - num_of_stages = 5 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/src/typedefs.jl b/src/typedefs.jl index 6778273c..f8bc2df7 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -139,7 +139,6 @@ Lax means that even if the Lagrangian dual is not solved as attempted due to bound stalling), the current values of the multipliers are used to define a valid cut for the value function, hoping that this cut will suffice to improve the current incumbent. -This only makes a difference if the cut type is LagrangianCut. Default is Rigorous. """ @@ -176,8 +175,6 @@ Bilinear means that by using strong duality the cuts are integrated in a bilinear way. This implies that MINLP solvers have to be used to solve the subproblems. Default is BigM. -Note that this only makes a differences if a binary approximation is used, - otherwise no projection is required at all. """ mutable struct BigM <: AbstractCutProjectionRegime end @@ -321,8 +318,6 @@ mutable struct AlgoParams infiltrate_state, ) end - - end # struct for solvers to be used From e0f9991a05dc5cb5f976739f76e0ac6c37f3a5de Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 15:54:37 -0400 Subject: [PATCH 04/30] Further changes on typedefs --- src/typedefs.jl | 113 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 33 deletions(-) diff --git a/src/typedefs.jl b/src/typedefs.jl index f8bc2df7..13a8f4f5 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -1,6 +1,6 @@ # The structs # > "NonlinearCut", -# > "InnerLoopIterationResult" +# > "IterationResult" # > "BackwardPassItems" # are derived from similar named structs (Cut, IterationResult, BackwardPassItems) # in the 'SDDP.jl' package by @@ -193,10 +193,13 @@ BinaryApproximation means that a dynamically refined binary approximation NoStateApproximation means that all cuts are generated in the original space, and thus may not be tight. Default is BinaryApproximation. + +Note that so far, the binary_precision is the same for all stages and only +differs in the state variables. """ mutable struct BinaryApproximation <: AbstractStateApproximationRegime - binary_precision::Dict{Symbol, Float64} # so far, binary precision is the same for all stages + binary_precision::Dict{Symbol, Float64} cut_projection_regime::AbstractCutProjectionRegime function BinaryApproximation(; binary_precision = ..., @@ -290,9 +293,8 @@ NoCutSelection means that no such procedure is used, so all cuts are used """ ################################################################################ +# DEFINING STRUCT FOR CONFIGURATION OF ALGORITHM PARAMETERS ################################################################################ - -# Mutable struct for algorithmic parameters mutable struct AlgoParams stopping_rules::Vector{SDDP.AbstractStoppingRule} state_approximation_regime::AbstractStateApproximationRegime @@ -320,61 +322,106 @@ mutable struct AlgoParams end end -# struct for solvers to be used +################################################################################ +# DEFINING STRUCT FOR SOLVERS TO BE USED +################################################################################ +""" +For the Lagrangian subproblems a separate solver can be defined if for + the LP/MILP solver numerical issues occur. +""" + struct AppliedSolvers LP :: Any MILP :: Any MINLP :: Any NLP :: Any - Lagrange :: Any # can be specified separately if numerical issues occur + Lagrange :: Any end -# Struct to store information on a nonlinear cut +################################################################################ +# DEFINING NONLINEAR CUTS +################################################################################ +""" +The first two arguments define the cut coefficients (in the binary space). + +The next four arguments store the trial_state from the forward pass, the +anchor_state at which the cut is constructed (and which may deviate from the +trial_state), the exact binary representation of the anchor_state and the +the binary precision at the moment the cut is constructed. +Actually, not all this information has to be stored. For example, the anchor_point +could as well be derived from binary_state and binary_precision. + +The next argument stores the value of the regularization parameter sigma at +the moment the cut is created. + +The next two arguments store references to the cut_variables and cut_constraints +(note that using the cut projection one cut refers to several such variables +and constraints). + +The next two arguments are SDDP-specific and not required in my case. + +Finally, the number of dominated cuts, which is required for the cut selection +process, and the current iteration number (to enumerate the cuts) is stored. + +Note that if no binary approximation is used, trial_state and anchor_state +will always be the same and only one cut_constraint has to be stored. +""" + mutable struct NonlinearCut - # cut coefficients (in binary space) + intercept::Float64 + coefficients::Dict{Symbol,Float64} ############################################################################ - intercept::Float64 # intercept of the cut (dual function value) - coefficients::Dict{Symbol,Float64} # optimal dual variables in binary space - # cut construction point - ############################################################################ - # NOTE: not sure if all of these have to be stored (anchor_state can be - # determined from other information for example) - trial_state::Dict{Symbol,Float64} # trial point at which cut should have been created - anchor_state::Dict{Symbol,Float64} # anchor point at which this cut was created - binary_state::Dict{Symbol,BinaryState} # binary representation of anchor point - binary_precision::Dict{Symbol,Float64} # binary precision at moment of creation - # sigma at moment of creation + trial_state::Dict{Symbol,Float64} + anchor_state::Dict{Symbol,Float64} + binary_state::Dict{Symbol,BinaryState} + binary_precision::Dict{Symbol,Float64} ############################################################################ sigma::Float64 - # references to variables and constraints of the cut projection closure ############################################################################ cut_variables::Vector{JuMP.VariableRef} cut_constraints::Vector{JuMP.ConstraintRef} - # SDDP-specific stuff ############################################################################ - obj_y::Union{Nothing,NTuple{N,Float64} where {N}} - belief_y::Union{Nothing,Dict{T,Float64} where {T}} - # number of non-dominated cuts (required for cut selection) + obj_y::Union{Nothing,NTuple{N,Float64} where {N}} #TODO + belief_y::Union{Nothing,Dict{T,Float64} where {T}} #TODO ############################################################################ - non_dominated_count::Int # SDDP - # iteration in which cut was created + non_dominated_count::Int ############################################################################ iteration::Int64 end -# mutable struct for iteration results +################################################################################ +# DEFINING STORAGE FOR ITERATION RESULTS +################################################################################ +""" +This is based on a similar struct in the SDDP package. It stores the results +corresponding to the current iteration. + +Note that the upper_bound is statistical only if we consider stochastic problems. +Also note that current_sol only contains the values of the state variables +in the current solution. + +Status refers to the number of iterations if I remember correctly. +Nonlinear_cuts is only required for logging, if I remember correctly. +""" + mutable struct IterationResult{T,S} lower_bound :: Float64 - upper_bound :: Float64 # statistical if we solve stochastic problems - current_sol :: Array{Dict{Symbol,Float64},1} # current state (also required for binary refinement) + upper_bound :: Float64 + current_sol :: Array{Dict{Symbol,Float64},1} scenario_path :: Vector{Tuple{T,S}} has_converged :: Bool - status :: Symbol # solution status (i.e. number of iterations) - nonlinear_cuts :: Dict{T, Vector{Any}} - # NOTE: only required for logging, binary expansion - # however, then also binary precision / K should be stored for interpretability + status :: Symbol #NOTE + nonlinear_cuts :: Dict{T, Vector{Any}} #NOTE end +################################################################################ +# DEFINING STORAGE FOR BACKWARD PASS RESULTS +################################################################################ +""" +This is based on a similar struct in the SDDP package. It stores items +corresponding to the current backward pass. +""" + struct BackwardPassItems{T,U} "Given a (node, noise) tuple, index the element in the array." cached_solutions::Dict{Tuple{T,Any},Int} From f7490045fd19d57a3a180dcef62a60173cf64142 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 20:05:31 -0400 Subject: [PATCH 05/30] Minor stuff for checks --- PreparationPackage.jl | 13 +++++++++++++ stuff.jl | 30 +++++++++++++++++------------- 2 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 PreparationPackage.jl diff --git a/PreparationPackage.jl b/PreparationPackage.jl new file mode 100644 index 00000000..5dc5fb16 --- /dev/null +++ b/PreparationPackage.jl @@ -0,0 +1,13 @@ +tpl = Template(; + user="ChrisFuelOR", + authors=["Christian Fuellner"], + julia=v"1.5", + plugins=[ + Git(; manifest=true), + Codecov(), + TravisCI(; x86=true), + Documenter{TravisCI}(), + ], +) + +tpl("DynamicSDDiP") diff --git a/stuff.jl b/stuff.jl index 5dc5fb16..22c57e62 100644 --- a/stuff.jl +++ b/stuff.jl @@ -1,13 +1,17 @@ -tpl = Template(; - user="ChrisFuelOR", - authors=["Christian Fuellner"], - julia=v"1.5", - plugins=[ - Git(; manifest=true), - Codecov(), - TravisCI(; x86=true), - Documenter{TravisCI}(), - ], -) - -tpl("DynamicSDDiP") +using SDDP + +abstract type Test end + +print(Test) +print(typeof(Test)) + +mutable struct Subtest <: Test end + +print(Subtest) +print(typeof(Subtest)) +print(typeof(Subtest) == Subtest) +print(Subtest == Subtest) +lim = SDDP.IterationLimit +print(lim) +print(typeof(lim)) +print(lim == SDDP.IterationLimit) From ffc359df1debf3df172ff34738ea3d0014eb061d Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 20:06:07 -0400 Subject: [PATCH 06/30] Changes to names and logging --- src/logging.jl | 129 +++++++++++++++++++----------------------------- src/typedefs.jl | 70 ++++++++++++++++++++++---- 2 files changed, 111 insertions(+), 88 deletions(-) diff --git a/src/logging.jl b/src/logging.jl index 2f039848..8fcc15ed 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -28,9 +28,6 @@ struct Log current_upper_bound::Float64 current_state::Vector{Dict{Symbol,Float64}} time::Float64 - #total_solves::Int - #sigma::Vector{Float64} - #binaryPrecision::Dict{Symbol,Float64} sigma_increased::Union{Bool,Nothing} binary_refinement::Union{Symbol,Nothing} subproblem_size::Union{Dict{Symbol,Int64},Nothing} @@ -47,68 +44,30 @@ end struct Options{T} # The initial state to start from the root node. initial_state::Dict{Symbol,Float64} - # The sampling scheme to use on the forward pass. - sampling_scheme::SDDP.AbstractSamplingScheme - backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme # Storage for the set of possible sampling states at each node. We only use # this if there is a cycle in the policy graph. starting_states::Dict{T,Vector{Dict{Symbol,Float64}}} - # Risk measure to use at each node. - risk_measures::Dict{T,SDDP.AbstractRiskMeasure} - # The delta by which to check if a state is close to a previously sampled - # state. - cycle_discretization_delta::Float64 - # Flag to add cuts to similar nodes. - refine_at_similar_nodes::Bool # The node transition matrix. Φ::Dict{Tuple{T,T},Float64} # A list of nodes that contain a subset of the children of node i. similar_children::Dict{T,Vector{T}} - stopping_rules::Vector{SDDP.AbstractStoppingRule} - dashboard_callback::Function - print_level::Int start_time::Float64 log::Vector{DynamicSDDiP.Log} - log_file_handle - log_frequency::Int - forward_pass::SDDP.AbstractForwardPass # Internal function: users should never construct this themselves. function Options( model::SDDP.PolicyGraph{T}, initial_state::Dict{Symbol,Float64}, - sampling_scheme::SDDP.AbstractSamplingScheme, - backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme, - risk_measures, - cycle_discretization_delta::Float64, - refine_at_similar_nodes::Bool, - stopping_rules::Vector{SDDP.AbstractStoppingRule}, - dashboard_callback::Function, - print_level::Int, start_time::Float64, log::Vector{DynamicSDDiP.Log}, - log_file_handle, - log_frequency::Int, - forward_pass::SDDP.AbstractForwardPass, ) where {T} return new{T}( initial_state, - sampling_scheme, - backward_sampling_scheme, SDDP.to_nodal_form(model, x -> Dict{Symbol,Float64}[]), - SDDP.to_nodal_form(model, risk_measures), - cycle_discretization_delta, - refine_at_similar_nodes, SDDP.build_Φ(model), SDDP.get_same_children(model), - stopping_rules, - dashboard_callback, - print_level, start_time, log, - log_file_handle, - log_frequency, - forward_pass, ) end end @@ -128,8 +87,8 @@ function print_banner(io) io, "--------------------------------------------------------------------------------", ) - println(io, " DynamicSDDiP.jl (c) Christian Füllner, 2021") - println(io, "re-uses code from SDDP.jl (c) Oscar Dowson, 2017-20") + println(io, "DynamicSDDiP.jl (c) Christian Füllner, 2021") + println(io, "re-uses code from SDDP.jl (c) Oscar Dowson, 2017-21") println(io) flush(io) end @@ -146,49 +105,64 @@ function print_parameters(io, algo_params::DynamicSDDiP.AlgoParams, applied_solv println(io) # Printing the parameters used + # TODO: Stopping rules println(io, Printf.@sprintf("opt_rtol: %1.4e", algo_params.opt_rtol)) println(io, Printf.@sprintf("opt_atol: %1.4e", algo_params.opt_atol)) println(io, Printf.@sprintf("iteration_limit: %5d", algo_params.iteration_limit)) println(io, Printf.@sprintf("time_limit (sec): %6d", algo_params.time_limit)) println(io, "------------------------------------------------------------------------") + print(io, "Binary approximation used: ") - println(io, algo_params.binary_approx) - print(io, "Initial binary precision: ") - println(io, algo_params.binary_precision) + println(io, algo_params.state_approximation_regime) + if algo_params.state_approximation_regime == DynamicSDDiP.BinaryApproximation + state_approximation_regime = algo_params.state_approximation_regime + print(io, "Initial binary precision: ") + println(io, state_approximation_regime.binary_precision) + print(io, "Cut projection method: ") + println(io, state_approximation_regime.cut_projection_method) + end + println(io, "------------------------------------------------------------------------") print(io, "Regularization used: ") - println(io, algo_params.regularization) - println(io, Printf.@sprintf("Initial sigma: %4.1e", algo_params.sigma)) - println(io, Printf.@sprintf("Sigma increase factor: %4.1e", algo_params.sigma:factor)) - println(io, "------------------------------------------------------------------------") - println(io, Printf.@sprintf("Lagrangian rtol: %1.4e", algo_params.lagrangian_rtol)) - println(io, Printf.@sprintf("Lagrangian atol: %1.4e", algo_params.lagrangian_atol)) - println(io, Printf.@sprintf("iteration_limit: %5d", algo_params.iteration_limit)) - print(io, "Dual initialization: ") - println(io, algo_params.dual_initialization_method) - print(io, "Dual solution method: ") - println(io, algo_params.dual_solution_method) - print(io, "Dual status regime: ") - println(io, algo_params.dual_status_regime) - print(io, "Dual bound regime: ") - println(io, algo_params.dual_bound_regime) - print(io, "Cut type: ") - println(io, algo_params.cut_type) - print(io, "Magnanti and Wong approach used: ") - println(io, algo_params.magnanti_wong) - print(io, "Numerical focus used: ") - println(io, algo_params.numerical_focus) + println(io, algo_params.regularization_regime) + if algo_params.regularization_regime == DynamicSDDiP.Regularization + println(io, Printf.@sprintf("Initial sigma: %4.1e", algo_params.sigma)) + println(io, Printf.@sprintf("Sigma increase factor: %4.1e", algo_params.sigma:factor)) + end + println(io, "------------------------------------------------------------------------") - if algo_params.dual_solution_method == :level_bundle - println(io, Printf.@sprintf("Level parameter: %2.4e", algo_params.bundle_params.level_factor)) - println(io, Printf.@sprintf("Bundle alpha: %2.4e", algo_params.bundle_params.bundle_alpha)) - println(io, Printf.@sprintf("Bundle factor: %2.4e", algo_params.bundle_params.bundle_factor)) + print(io, "Cut family used: ") + println(io, algo_params.cut_family_regime) + if algo_params.cut_family_regime == DynamicSDDiP.LagrangianCut + cut_family_regime = algo_params.cut_family_regime + print(io, "Dual initialization: ") + println(io, cut_family_regime.dual_initialization_regime) + print(io, "Dual bounding: ") + println(io, cut_family_regime.dual_bound_regime) + print(io, "Dual solution method: ") + println(io, cut_family_regime.dual_solution_regime) + print(io, "Dual multiplier choice: ") + println(io, cut_family_regime.dual_choice_regime) + print(io, "Dual status regime: ") + println(io, cut_family_regime.dual_status_regime) + #print(io, "Numerical focus used: ") + #println(io, cut_family_regime.numerical_focus) + println(io, "------------------------------------------------------------------------") + dual_solution_regime = cut_family_regime.dual_solution_regime + println(io, Printf.@sprintf("Lagrangian rtol: %1.4e", dual_solution_regime.rtol)) + println(io, Printf.@sprintf("Lagrangian atol: %1.4e", dual_solution_regime.atol)) + println(io, Printf.@sprintf("iteration_limit: %5d", dual_solution_regime.iteration_limit)) + if dual_solution_regime == DynamicSDDiP.LevelBundle + println(io, Printf.@sprintf("Level parameter: %2.4e", dual_solution_regime.level_factor)) + println(io, Printf.@sprintf("Bundle alpha: %2.4e", dual_solution_regime.bundle_alpha)) + println(io, Printf.@sprintf("Bundle factor: %2.4e", dual_solution_regime.bundle_factor)) + end println(io, "------------------------------------------------------------------------") + end - print(io, "Cut projection method: ") - println(io, algo_params.cut_projection_method) + print(io, "Cut selection used: ") - println(io, algo_params.cut_selection) + println(io, algo_params.cut_selection_regime) println(io, "------------------------------------------------------------------------") print(io, Printf.@sprintf("LP solver: %15s", applied_solvers.LP)) print(io, Printf.@sprintf("MILP solver: %15s", applied_solvers.MILP)) @@ -279,10 +253,9 @@ function print_footer(io, training_results) flush(io) end -function log_iteration(options, log) - options.dashboard_callback(log[end], false) - if options.print_level > 0 && mod(length(log), options.log_frequency) == 0 - print_helper(print_iteration, options.log_file_handle, log[end]) +function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log) + if algo_params.print_level > 0 && mod(length(log), algo_params.log_frequency) == 0 + print_helper(print_iteration, algo_params.log_file_handle, log[end]) end end diff --git a/src/typedefs.jl b/src/typedefs.jl index 13a8f4f5..b5e1aa35 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -242,11 +242,11 @@ Default is Regularization. ################################################################################ # CUT FAMILY TO BE USED ################################################################################ -abstract type AbstractCutTypeRegime end +abstract type AbstractCutFamilyRegime end -mutable struct LagrangianCut <: AbstractCutTypeRegime end +mutable struct LagrangianCut <: AbstractCutFamilyRegime end -mutable struct LagrangianCut <: AbstractCutTypeRegime +mutable struct LagrangianCut <: AbstractCutFamilyRegime dual_initialization_regime::AbstractDualInitializationRegime dual_bound_regime::AbstractDualBoundRegime dual_solution_regime::AbstractDualSolutionRegime @@ -264,8 +264,8 @@ mutable struct LagrangianCut <: AbstractCutTypeRegime end end -mutable struct BendersCut <: AbstractCutTypeRegime end -mutable struct StrengthenedCut <: AbstractCutTypeRegime end +mutable struct BendersCut <: AbstractCutFamilyRegime end +mutable struct StrengthenedCut <: AbstractCutFamilyRegime end """ LagrangianCut means that the Lagrangian dual is (approximately) solved to obtain @@ -282,7 +282,14 @@ Default is LagrangianCut. ################################################################################ abstract type AbstractCutSelectionRegime end -mutable struct CutSelection <: AbstractCutSelectionRegime end +mutable struct CutSelection <: AbstractCutSelectionRegime + cut_deletion_minimum::Int + function CutSelection(; + cut_deletion_minimum = 1, + ) + return new(cut_deletion_minimum) + end + mutable struct NoCutSelection <: AbstractCutSelectionRegime end """ @@ -295,28 +302,71 @@ NoCutSelection means that no such procedure is used, so all cuts are used ################################################################################ # DEFINING STRUCT FOR CONFIGURATION OF ALGORITHM PARAMETERS ################################################################################ +""" +Note that the parameters from risk_measure to refine_at_similar_nodes are +basic SDDP parameters which are required as we are using some functionality +from the package SDDP.jl. They should not be changed, though, as for different +choices the DynamicSDDiP algorithm will not work. +""" + mutable struct AlgoParams stopping_rules::Vector{SDDP.AbstractStoppingRule} state_approximation_regime::AbstractStateApproximationRegime regularization_regime::AbstractRegularizationRegime - cut_type_regime::AbstractCutTypeRegime + cut_family_regime::AbstractCutFamilyRegime cut_selection_regime::AbstractCutSelectionRegime + ############################################################################ + risk_measure = SDDP.Expectation() + forward_pass::SDDP.AbstractForwardPass + sampling_scheme::SDDP.AbstractSamplingScheme + backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme + parallel_scheme::SDDP.AbstractParallelScheme + cut_type::SDDP.CutType + refine_at_similar_nodes::Bool + cycle_discretization_delta::Float64 + ############################################################################ + print_level::Int + log_frequency::Int + log_file::String + run_numerical_stability_report::Bool infiltrate_state::Symbol function AlgoParams(; stopping_rules = [DeterministicStopping()], state_approximation_regime = BinaryApproximation(), regularization_regime = Regularization(), - cut_type_regime = LagrangianCut(), + cut_family_regime = LagrangianCut(), cut_selection_regime = CutSelection(), - infiltrate_state = :None + forward_pass = SDDP.DefaultForwardPass(), + sampling_scheme = SDDP.InSampleMonteCarlo(), + backward_sampling_scheme = SDDP.CompleteSampler(), + parallel_scheme = SDDP.Serial(), + cut_type = SDDP.SINGLE_CUT, + refine_at_similar_nodes = true, + cycle_discretization_delta = 0.0, + print_level = 1, + log_frequency = 1, + log_file = "DynamicSDDiP.log", + run_numerical_stability_report = true, + infiltrate_state = :None, ) return new( stopping_rules, state_approximation_regime, regularization_regime, - cut_type_regime, + cut_family_regime, cut_selection_regime, + forward_pass, + sampling_scheme, + backward_sampling_scheme, + parallel_scheme, + cut_type, + refine_at_similar_nodes, + cycle_discretization_delta, + print_level, + log_frequency, + log_file, + run_numerical_stability_report infiltrate_state, ) end From c9eed6e1ac911ba1e3fc2a0cb9316f3aa3c0dabc Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 7 Oct 2021 20:08:00 -0400 Subject: [PATCH 07/30] Split up algorithm.jl into FP and BP. Changed solveStarter.jl to algorithmMain.jl and included iteration(). Used new abstract types for regularization and binary refinement checks. Adapted sigma test accordingly and moved them it to a separate file. --- src/algorithmMain.jl | 554 ++++++++++++++++++ src/{algorithm.jl => backwardPass.jl} | 530 +---------------- ...roblemModifications.jl => binarization.jl} | 278 --------- src/binaryRefinement.jl | 107 ++++ src/forwardPass.jl | 154 +++++ src/regularizations.jl | 107 +--- src/sigmaTest.jl | 118 ++++ src/solveStarter.jl | 305 ---------- 8 files changed, 958 insertions(+), 1195 deletions(-) create mode 100644 src/algorithmMain.jl rename src/{algorithm.jl => backwardPass.jl} (52%) rename src/{problemModifications.jl => binarization.jl} (56%) create mode 100644 src/binaryRefinement.jl create mode 100644 src/forwardPass.jl create mode 100644 src/sigmaTest.jl delete mode 100644 src/solveStarter.jl diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl new file mode 100644 index 00000000..6436c251 --- /dev/null +++ b/src/algorithmMain.jl @@ -0,0 +1,554 @@ +# The functions +# > "solve", +# > "master_loop" +# > "inner_loop" +# > "iteration" +# are derived from similar named functions (solve, master_loop, iteration) in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Solves the `model`. In contrast to SDDP.jl, all parameters configuring +the algorithm are given (and possibly pre-defined) in algo_params. +""" +function solve( + model::SDDP.PolicyGraph, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers +) + + ############################################################################ + # INITIALIZATION (SIMILAR TO SDDP.jl) + ############################################################################ + # Reset the TimerOutput + TimerOutputs.reset_timer!(DynamicSDDiP_TIMER) + + # Prepare logging + log_file_handle = open(algo_params.log_file, "a") + log = Log[] + + if print_level > 0 + print_helper(print_banner, log_file_handle) + end + + if print_level > 1 + print_helper(print_parameters, log_file_handle, algo_params, applied_solvers) + end + + # Maybe add run_numerical_stability_report as in SDDP.jl later + + # Prepare stopping rules + #--------------------------------------------------------------------------- + # Convert the vector to an AbstractStoppingRule. Otherwise if the user gives + # something like stopping_rules = [SDDP.IterationLimit(100)], the vector + # will be concretely typed and we can't add a TimeLimit. + #stopping_rules = algo_params.stopping_rules + #stopping_rules = convert(Vector{SDDP.AbstractStoppingRule}, stopping_rules) + if length(algo_params.stopping_rules) == 0 + @warn( + "You haven't specified a stopping rule! You can only terminate " * + "the call to DynamicSDDiP.solve via a keyboard interrupt ([CTRL+C])." + ) + end + + # Prepare options for logging + #--------------------------------------------------------------------------- + options = DynamicSDDiP.Options( + model, + model.initial_root_state, + time(), + log, + ) + + ############################################################################ + # RE-INITIALIZE THE EXISTING VALUE FUNCTION AND PREPARE CUT SELECTION + ############################################################################ + # Update the nodes with the selected cut type (SINGLE_CUT or MULTI_CUT) + # and the cut deletion minimum. + if algo_params.cut_selection_regime.cut_deletion_minimum < 0 + algo_params.cut_selection_regime.cut_deletion_minimum = typemax(Int) + end + + # Change to nonconvex Bellman function to use non-convex cuts + #--------------------------------------------------------------------------- + # fortunately, node.bellman_function requires no specific type + for (key, node) in model.nodes + node.bellman_function = initialize_bellman_function_nonconvex(bellman_function, model, node) + node.bellman_function.cut_type = algo_params.cut_type + node.bellman_function.global_theta.cut_oracle.deletion_minimum = + algo_params.cut_selection_regime.cut_deletion_minimum + for oracle in node.bellman_function.local_thetas + oracle.cut_oracle.deletion_minimum = algo_params.cut_selection_regime.cut_deletion_minimum + end + end + + # MODEL START + ############################################################################ + status = :not_solved + try + status = solve_DynamicSDDiP(parallel_scheme, model, options, algo_params, applied_solvers) + catch ex + if isa(ex, InterruptException) + status = :interrupted + interrupt(parallel_scheme) + else + close(log_file_handle) + rethrow(ex) + end + finally + end + + # lOG MODEL RESULTS + ############################################################################ + results = DynamicSDDiP.Results(status, log) + model.ext[:results] = results + if print_level > 0 + print_helper(print_footer, log_file_handle, results) + if print_level > 1 + print_helper(TimerOutputs.print_timer, log_file_handle, DynamicSDDiP_TIMER) + print_helper(println, log_file_handle) + end + end + close(log_file_handle) + return +end + +""" +Solves the `model` using DynamicSDDiP in a serial scheme. +""" + +function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, + options::DynamicSDDiP.Options, algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers) where {T} + + ############################################################################ + # SET UP STATE VARIABLE INFORMATION + ############################################################################ + for (node_index, children) in model.nodes + node = model.nodes[node_index] + + # Set info for state.in as for previous stage's state.out + #----------------------------------------------------------------------- + if node_index > 1 + for (i, (name, state)) in enumerate(node.states) + # Get correct state_info + state_info = model.nodes[node_index-1].states[name].info.out + + if state_info.has_lb + JuMP.set_lower_bound(state.in, state_info.lower_bound) + end + if state_info.has_ub + JuMP.set_upper_bound(state.in, state_info.upper_bound) + end + if state_info.binary + JuMP.set_binary(state.in) + elseif state_info.integer + JuMP.set_integer(state.in) + end + + # Store info to reset it later + state.info.in = state_info + end + end + end + + @infiltrate algo_params.infiltrate_state == :all + + ############################################################################ + # CALL ACTUAL SOLUTION PROCEDURE + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "loop" begin + status = master_loop(parallel_scheme, model, options, algo_params, applied_solvers) + end + return status + +end + + +""" +Loop function of DynamicSDDiP. +""" + +function master_loop(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, + options::DynamicSDDiP.Options, algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + regularization_regime::DynamicSDDiP.Regularization) where {T} + + ############################################################################ + # INITIALIZE PARAMETERS REQUIRED FOR REFINEMENTS + ############################################################################ + previous_solution = nothing + previous_bound = nothing + sigma_increased = false + bound_check = false + + ############################################################################ + # INITIALIZE BEST KNOWN POINT AND OBJECTIVE VALUE + ############################################################################ + model.ext[:best_objective] = model.objective_sense == JuMP.MOI.MIN_SENSE ? Inf : -Inf + model.ext[:best_point] = Vector{Dict{Symbol,Float64}}() + + ############################################################################ + # ACTUAL LOOP + ############################################################################ + while true + # start an iteration + TimerOutputs.@timeit DynamicSDDiP_TIMER "iterations" begin + result = iteration(model, options, algo_params, applied_solvers, previous_solution, bound_check, sigma_increased) + end + + # logging + log_iteration(algo_params, options.log) + + # initialize parameters + previous_solution = result.current_sol + previous_bound = result.lower_bound + sigma_increased = false + bound_check = true + + @infiltrate algo_params.infiltrate_state in [:all, :sigma] + + # check for convergence and if not achieved, update parameters + convergence_results = convergence_handler( + result, + model, + options, + algo_params, + applied_solvers, + sigma_increased, + bound_check, + previous_solution, + previous_bound, + algo_params.regularization_regime + ) + + previous_solution = convergence_results.previous_solution + previous_bound = convergence_results.previous_bound + sigma_increased = convergence_results.sigma_increased + bound_check = convergence_results.bound_check + + end + +end + + +""" +Convergence handler if regularization is used. +""" + +function convergence_handler(result::DynamicSDDiP.IterationResult, + model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + sigma_increased::Bool, bound_check::Bool, + previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, + previous_bound::Union{Float64,Nothing}, + regularization_regime::DynamicSDDiP.Regularization) where {T} + + ############################################################################ + # IF CONVERGENCE IS ACHIEVED, COMPARE TRUE AND REGULARIZED PROBLEM + ############################################################################ + if result.has_converged + TimerOutputs.@timeit DynamicSDDiP_TIMER "sigma_test" begin + sigma_test_results = forward_sigma_test(model, options, algo_params, applied_solvers, result.scenario_path, sigma_increased) + end + sigma_increased = sigma_test_results.sigma_increased + @infiltrate algo_params.infiltrate_state in [:all, :sigma] + + if sigma_increased + # reset previous values, as model is changed and convergence not achieved + previous_solution = nothing + previous_bound = nothing + # binary refinement only when no sigma refinement has been made + bound_check = false + else + #################################################################### + # THE ALGORITHM TERMINATES + #################################################################### + # return convergence status + return result.status + end + + ############################################################################ + # IF NO CONVERGENCE IS ACHIEVED, DO DIFFERENT CHECKS + ############################################################################ + else + # CHECK IF LOWER BOUND HAS IMPROVED + ######################################################################## + # NOTE: If not, then the cut was (probably) not tight enough, + # so the binary approximation should be refined in the next iteration. + # As different trial states could yield the same lower bound, this + # is only done if also the trial state does not change, though. + if !isnothing(previous_bound) + if !isapprox(previous_bound, result.lower_bound) + bound_check = false + end + else + bound_check = false + end + + # CHECK IF SIGMA SHOULD BE INCREASED (DUE TO LB > UB) + ######################################################################## + if result.upper_bound - result.lower_bound < - 1e-8 # NOTE + regularization_regime.sigma = regularization_regime.sigma * regularization_regime.sigma_factor + sigma_increased = true + previous_solution = nothing + previous_bound = nothing + bound_check = false + else + sigma_increased = false + end + + end + + return ( + sigma_increased = sigma_increased, + previous_solution = previous_solution, + previous_bound = previous_bound, + bound_check = bound_check + ) +end + + +""" +Convergence handler if regularization is not used. +""" + +function convergence_handler(result::DynamicSDDiP.IterationResult, + model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + sigma_increased::Bool, bound_check::Bool, + previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, + previous_bound::Union{Float64,Nothing}, + regularization_regime::DynamicSDDiP.NoRegularization) where {T} + + ############################################################################ + # IF CONVERGENCE IS ACHIEVED, COMPARE TRUE AND REGULARIZED PROBLEM + ############################################################################ + if result.has_converged + @infiltrate algo_params.infiltrate_state in [:all, :sigma] + ######################################################################## + # THE ALGORITHM TERMINATES + ######################################################################## + return result.status + + ############################################################################ + # IF NO CONVERGENCE IS ACHIEVED, DO DIFFERENT CHECKS + ############################################################################ + else + # CHECK IF LOWER BOUND HAS IMPROVED + ######################################################################## + # If not, then the cut was (probably) not tight enough, + # so the binary approximation should maybe be refined in the next iteration. + if !isnothing(previous_bound) + if !isapprox(previous_bound, result.lower_bound) + bound_check = false + end + else + bound_check = false + end + + # CHECK IF LB > UB + ######################################################################## + if result.upper_bound - result.lower_bound < - 1e-8 # NOTE + error("LB < UB for DynamicSDDiP. Terminating.") + end + + end + + return ( + sigma_increased = sigma_increased, + previous_solution = previous_solution, + previous_bound = previous_bound, + bound_check = bound_check + ) +end + + +# The functions +# > "iteration", +# > "forward_pass", +# > "solve_subproblem_forward", +# > "solve_all_children", +# > "solve_subproblem_backward", +# > "get_dual_variables_backward", +# > "calculate_bound", +# > "solve_first_stage_problem" +# are derived from similar named functions (iteration, forward_pass, backward_pass, +# solve_all_children, solve_subproblem, get_dual_variables, calculate_bound, +# solve_first_stage_problem) in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Executing an inner loop iteration for DynamicSDDiP. +""" +function iteration( + model::SDDP.PolicyGraph{T}, + options::DynamicSDDiP.Options, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, + bound_check::Bool, + sigma_increased::Bool + ) where {T} + + ############################################################################ + # SET ITERATION COUNTER + ############################################################################ + if haskey(model.ext, :iteration) + model.ext[:iteration] += 1 + else + model.ext[:iteration] = 1 + end + + ############################################################################ + # FORWARD PASS + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "forward_pass" begin + forward_trajectory = DynamicSDDiP.forward_pass(model, options, algo_params, applied_solvers, algo_params.forward_pass) + end + + ############################################################################ + # BINARY REFINEMENT + ############################################################################ + # If the forward pass solution and the lower bound did not change during + # the last iteration, then increase the binary precision (for all stages) + refinement_check = false + binary_refinement = :none + + TimerOutputs.@timeit DynamicSDDiP_TIMER "bin_refinement" begin + if !isnothing(previousSolution) && bound_check + refinement_check = DynamicSDDiP.binary_refinement_check( + model, + previous_solution, + forward_trajectory.sampled_states, + refinement_check, + algo_params.state_approximation_regime + ) + end + if refinement_check + binary_refinement = DynamicSDDiP.binary_refinement( + model, + algo_params.state_approximation_regime.binary_precision, + binary_refinement + ) + end + end + # bound_check = true + @infiltrate algo_params.infiltrate_state in [:all] + + ############################################################################ + # BACKWARD PASS + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "backward_pass" begin + cuts = DynamicSDDiP.backward_pass( + model, + options, + algo_params, + applied_solvers, + forward_trajectory.scenario_path, + forward_trajectory.sampled_states, + forward_trajectory.objective_states, + forward_trajectory.belief_states, + ) + end + + ############################################################################ + # CALCULATE LOWER BOUND + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "calculate_bound" begin + first_stage_results = calculate_bound(model) + end + bound = first_stage_results.bound + + ############################################################################ + # CHECK IF BEST KNOWN SOLUTION HAS BEEN IMPROVED + ############################################################################ + if model.objective_sense == JuMP.MOI.MIN_SENSE + if forward_trajectory.cumulative_value < model.ext[:best_objective] || sigma_increased + # udpate best upper bound + model.ext[:best_objective] = forward_trajectory.cumulative_value + # update best point so far + model.ext[:best_point] = forward_trajectory.sampled_states + end + else + if forward_trajectory.cumulative_value > model.ext[:best_objective] || sigma_increased + # udpate best lower bound + model.ext[:best_objective] = forward_trajectory.cumulative_value + # update best point so far + model.ext[:best_point] = forward_trajectory.sampled_states + end + end + + ############################################################################ + # PREPARE LOGGING + ############################################################################ + subproblem_size = first_stage_results.problem_size[1] + model.ext[:total_cuts] = 0 + model.ext[:active_cuts] = 0 + + for (node_index, children) in model.nodes + node = model.nodes[node_index] + + if length(node.children) == 0 + continue + end + model.ext[:total_cuts] += node.ext[:total_cuts] + model.ext[:active_cuts] += node.ext[:active_cuts] + end + + push!( + options.log, + Log( + model.ext[:iteration], + bound, + model.ext[:best_objective], + forward_trajectory.cumulative_value, + forward_trajectory.sampled_states, + time() - options.start_time, + sigma_increased, + binary_refinement, + subproblem_size, + algo_params, + model.ext[:lag_iterations], + model.ext[:lag_status], + model.ext[:total_cuts], + model.ext[:active_cuts], + ), + ) + + ############################################################################ + # CHECK IF THE LOOP CONVERGED YET + ############################################################################ + has_converged, status = convergence_test(model, options.log, algo_params.stopping_rules) + + @infiltrate algo_params.infiltrate_state in [:all] + + return DynamicSDDiP.IterationResult( + bound, + model.ext[:best_objective], + model.ext[:best_point], + forward_trajectory.scenario_path, + has_converged, + status, + cuts, + ) +end diff --git a/src/algorithm.jl b/src/backwardPass.jl similarity index 52% rename from src/algorithm.jl rename to src/backwardPass.jl index f5068966..a50374ec 100644 --- a/src/algorithm.jl +++ b/src/backwardPass.jl @@ -1,15 +1,10 @@ # The functions -# > "iteration", -# > "forward_pass", -# > "solve_subproblem_forward", # > "solve_all_children", # > "solve_subproblem_backward", # > "get_dual_variables_backward", # > "calculate_bound", # > "solve_first_stage_problem" -# > "forward_sigma_test" -# > "solve_subproblem_sigma_test" -# are derived from similar named functions (iteration, forward_pass, backward_pass, +# are derived from similar named functions (backward_pass, # solve_all_children, solve_subproblem, get_dual_variables, calculate_bound, # solve_first_stage_problem) in the 'SDDP.jl' package by # Oscar Dowson and released under the Mozilla Public License 2.0. @@ -23,341 +18,6 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ -""" -Executing an inner loop iteration for DynamicSDDiP. -""" -function iteration( - model::SDDP.PolicyGraph{T}, - options::DynamicSDDiP.Options, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers, - previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, - bound_check::Bool, - sigma_increased::Bool - ) where {T} - - # ITERATION COUNTER - ############################################################################ - if haskey(model.ext, :iteration) - model.ext[:iteration] += 1 - else - model.ext[:iteration] = 1 - end - - # FORWARD PASS - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "forward_pass" begin - forward_trajectory = DynamicSDDiP.forward_pass(model, options, algo_params, applied_solvers, options.forward_pass) - end - - # BINARY REFINEMENT - ############################################################################ - # If the forward pass solution and the lower bound did not change during - # the last iteration, then increase the binary precision (for all stages) - solution_check = true - binary_refinement = :none - - if !isnothing(previousSolution) - TimerOutputs.@timeit DynamicSDDiP_TIMER "bin_refinement" begin - solution_check = DynamicSDDiP.binary_refinement_check(model, previous_solution, forward_trajectory.sampled_states, solution_check) - if solution_check && bound_check - # Increase binary precision such that K = K + 1 - binary_refinement = DynamicSDDiP.binary_refinement(model, algo_params, binary_refinement) - end - end - end - bound_check = true - @infiltrate algo_params.infiltrate_state in [:all] - - # BACKWARD PASS - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "backward_pass" begin - cuts = DynamicSDDiP.backward_pass( - model, - options, - algo_params, - applied_solvers, - forward_trajectory.scenario_path, - forward_trajectory.sampled_states, - forward_trajectory.objective_states, - forward_trajectory.belief_states, - ) - end - - # CALCULATE LOWER BOUND - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "calculate_bound" begin - first_stage_results = calculate_bound(model) - end - bound = first_stage_results.bound - - # CHECK IF BEST KNOWN SOLUTION HAS BEEN IMPROVED - ############################################################################ - if model.objective_sense == JuMP.MOI.MIN_SENSE - if forward_trajectory.cumulative_value < model.ext[:best_objective] || sigma_increased - # udpate best upper bound - model.ext[:best_objective] = forward_trajectory.cumulative_value - # update best point so far - model.ext[:best_point] = forward_trajectory.sampled_states - end - else - if forward_trajectory.cumulative_value > model.ext[:best_objective] || sigma_increased - # udpate best lower bound - model.ext[:best_objective] = forward_trajectory.cumulative_value - # update best point so far - model.ext[:best_point] = forward_trajectory.sampled_states - end - end - - # PREPARE LOGGING - ############################################################################ - subproblem_size = first_stage_results.problem_size[1] - model.ext[:total_cuts] = 0 - model.ext[:active_cuts] = 0 - - for (node_index, children) in model.nodes - node = model.nodes[node_index] - - if length(node.children) == 0 - continue - end - model.ext[:total_cuts] += node.ext[:total_cuts] - model.ext[:active_cuts] += node.ext[:active_cuts] - end - - push!( - options.log, - Log( - model.ext[:iteration], - bound, - model.ext[:best_objective], - forward_trajectory.cumulative_value, - forward_trajectory.sampled_states, - time() - options.start_time, - sigma_increased, - binary_refinement, - subproblem_size, - algo_params, - model.ext[:lag_iterations], - model.ext[:lag_status], - model.ext[:total_cuts], - model.ext[:active_cuts], - ), - ) - - # CHECK IF THE LOOP CONVERGED YET - ############################################################################ - has_converged, status = convergence_test(model, options.log, options.stopping_rules) - - @infiltrate algo_params.infiltrate_state in [:all] - - return DynamicSDDiP.IterationResult( - bound, - model.ext[:best_objective], - model.ext[:best_point], - forward_trajectory.scenario_path, - has_converged, - status, - cuts, - ) -end - - -""" -Executing the forward pass of an inner loop iteration for DynamicSDDiP. -""" -function forward_pass(model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, - algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, - ::SDDP.DefaultForwardPass) where {T} - - # SAMPLING AND INITIALIZATION (JUST LIKE IN SDDP) - ############################################################################ - # First up, sample a scenario. Note that if a cycle is detected, this will - # return the cycle node as well. - scenario_path, terminated_due_to_cycle = SDDP.sample_scenario(model, options.sampling_scheme) - - # Storage for the list of outgoing states that we visit on the forward pass. - sampled_states = Dict{Symbol,Float64}[] - # Storage for the belief states: partition index and the belief dictionary. - belief_states = Tuple{Int,Dict{T,Float64}}[] - current_belief = SDDP.initialize_belief(model) - # Our initial incoming state. - incoming_state_value = copy(model.initial_root_state) - # A cumulator for the stage-objectives. - cumulative_value = 0.0 - # Objective state interpolation. - objective_state_vector, N = SDDP.initialize_objective_state(model[scenario_path[1][1]]) - objective_states = NTuple{N,Float64}[] - - # ACTUAL ITERATION - ######################################################################## - # Iterate down the scenario tree. - for (depth, (node_index, noise)) in enumerate(scenario_path) - node = model[node_index] - - # Objective state interpolation. - objective_state_vector = - SDDP.update_objective_state(node.objective_state, objective_state_vector, noise) - if objective_state_vector !== nothing - push!(objective_states, objective_state_vector) - end - # Update belief state, etc. - if node.belief_state !== nothing - belief = node.belief_state::SDDP.BeliefState{T} - partition_index = belief.partition_index - current_belief = - belief.updater(belief.belief, current_belief, partition_index, noise) - push!(belief_states, (partition_index, copy(current_belief))) - end - # ===== Begin: starting state for infinite horizon ===== - starting_states = options.starting_states[node_index] - if length(starting_states) > 0 - # There is at least one other possible starting state. If our - # incoming state is more than δ away from the other states, add it - # as a possible starting state. - if distance(starting_states, incoming_state_value) > - options.cycle_discretization_delta - push!(starting_states, incoming_state_value) - end - # TODO(odow): - # - A better way of randomly sampling a starting state. - # - Is is bad that we splice! here instead of just sampling? For - # convergence it is probably bad, since our list of possible - # starting states keeps changing, but from a computational - # perspective, we don't want to keep a list of discretized points - # in the state-space δ distance apart... - incoming_state_value = splice!(starting_states, rand(1:length(starting_states))) - end - # ===== End: starting state for infinite horizon ===== - - # Set sigma for regularization - sigma = algo_params.sigma[node_index] - - # SET SOLVER - ######################################################################## - DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) - - # SUBPROBLEM SOLUTION - ############################################################################ - # Solve the subproblem, note that `require_duals = false`. - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_FP" begin - subproblem_results = solve_subproblem_forward( - model, - node, - node_index, - incoming_state_value, # only values, no State struct! - noise, - scenario_path[1:depth], - sigma, - algo_params.infiltrate_state, - ) - end - # Cumulate the stage_objective. - cumulative_value += subproblem_results.stage_objective - # Set the outgoing state value as the incoming state value for the next - # node. - incoming_state_value = copy(subproblem_results.state) - # Add the outgoing state variable to the list of states we have sampled - # on this forward pass. - push!(sampled_states, incoming_state_value) - - end - if terminated_due_to_cycle - # Get the last node in the scenario. - final_node_index = scenario_path[end][1] - # We terminated due to a cycle. Here is the list of possible starting - # states for that node: - starting_states = options.starting_states[final_node_index] - # We also need the incoming state variable to the final node, which is - # the outgoing state value of the last node: - incoming_state_value = sampled_states[end] - # If this incoming state value is more than δ away from another state, - # add it to the list. - if distance(starting_states, incoming_state_value) > - options.cycle_discretization_delta - push!(starting_states, incoming_state_value) - end - end - - # ===== End: drop off starting state if terminated due to cycle ===== - return ( - scenario_path = scenario_path, - sampled_states = sampled_states, - objective_states = objective_states, - belief_states = belief_states, - cumulative_value = cumulative_value, - ) -end - - -""" -Solving the subproblem within the forward pass of a loop of DynamicSDDiP. -""" -# Internal function: solve the subproblem associated with node given the -# incoming state variables state and realization of the stagewise-independent -# noise term noise. If require_duals=true, also return the dual variables -# associated with the fixed constraint of the incoming state variables. -function solve_subproblem_forward( - model::SDDP.PolicyGraph{T}, - node::SDDP.Node{T}, - node_index::Int64, - state::Dict{Symbol,Float64}, - noise, - scenario_path::Vector{Tuple{T,S}}, - sigma::Float64, - infiltrate_state::Symbol; -) where {T,S} - - subproblem = node.subproblem - - # MODEL PARAMETRIZATION - ############################################################################ - # Parameterize the model. First, fix the value of the incoming state - # variables. Then parameterize the model depending on `noise`. Finally, - # set the objective. - set_incoming_state(node, state) - parameterize(node, noise) - - # REGULARIZE SUBPROBLEM - ############################################################################ - if node_index > 1 && algo_params.regularization - node.ext[:regularization_data] = Dict{Symbol,Any}() - regularize_subproblem!(node, subproblem, sigma) - end - - # SOLUTION - ############################################################################ - @infiltrate infiltrate_state in [:all] - JuMP.optimize!(subproblem) - - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - - # NOTE: TO-DO: Attempt numerical recovery as in SDDP - - state = get_outgoing_state(node) - objective = JuMP.objective_value(subproblem) - stage_objective = objective - JuMP.value(bellman_term(node.bellman_function)) - @infiltrate infiltrate_state in [:all] - - # DE-REGULARIZE SUBPROBLEM - ############################################################################ - if node_index > 1 && algo_params.regularization - deregularize_subproblem!(node, subproblem) - end - - return ( - state = state, - duals = dual_values, - objective = objective, - stage_objective = stage_objective, - ) -end - - """ Executing the backward pass of a loop of DynamicSDDiP. """ @@ -917,191 +577,3 @@ function solve_first_stage_problem( problem_size = problem_size ) end - - -""" -Executing a forward pass to check if parameter sigma is chosen sufficiently -large in DynamicSDDiP. -""" -function forward_sigma_test( - model::SDDP.PolicyGraph{T}, options::NCNBD.Options, - algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, - scenario_path::Vector{Tuple{T,S}}, ::SDDP.DefaultForwardPass, - sigma_increased::Bool) where {T,S} - - # INITIALIZATION (NO SAMPLING HERE!) - ############################################################################ - # Storage for the list of outgoing states that we visit on the forward pass. - sampled_states = Dict{Symbol,Float64}[] - # Storage for the belief states: partition index and the belief dictionary. - belief_states = Tuple{Int,Dict{T,Float64}}[] - current_belief = SDDP.initialize_belief(model) - # Our initial incoming state. - incoming_state_value = copy(model.ext[:lin_initial_root_state]) - # A cumulator for the stage-objectives. - cumulative_value = 0.0 - # Objective state interpolation. - objective_state_vector, N = SDDP.initialize_objective_state(model[scenario_path[1][1]]) - objective_states = NTuple{N,Float64}[] - - # ACTUAL ITERATION - ######################################################################## - # Iterate down the scenario tree. - for (depth, (node_index, noise)) in enumerate(scenario_path) - node = model[node_index] - - # Objective state interpolation. - objective_state_vector = - SDDP.update_objective_state(node.objective_state, objective_state_vector, noise) - if objective_state_vector !== nothing - push!(objective_states, objective_state_vector) - end - # Update belief state, etc. - if node.belief_state !== nothing - belief = node.belief_state::SDDP.BeliefState{T} - partition_index = belief.partition_index - current_belief = - belief.updater(belief.belief, current_belief, partition_index, noise) - push!(belief_states, (partition_index, copy(current_belief))) - end - # ===== Begin: starting state for infinite horizon ===== - starting_states = options.starting_states[node_index] - if length(starting_states) > 0 - # There is at least one other possible starting state. If our - # incoming state is more than δ away from the other states, add it - # as a possible starting state. - if distance(starting_states, incoming_state_value) > - options.cycle_discretization_delta - push!(starting_states, incoming_state_value) - end - - incoming_state_value = splice!(starting_states, rand(1:length(starting_states))) - end - # ===== End: starting state for infinite horizon ===== - - # SET SOLVER - ######################################################################## - DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) - - # SOLVE REGULARIZED PROBLEM - ############################################################################ - # This has to be done in this sigma test again, since the current upper bound - # may not be the best upper bound and thus, just comparing the bounds is not - # sufficient. Moreover, we do it in this loop to not increase sigma for - # all stages, but only where it is needed - - # Solve the subproblem, note that `require_duals = false`. - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_sigma_test_reg" begin - reg_results = solve_subproblem_forward( - model, - node, - node_index, - incoming_state_value, # only values, no State struct! - noise, - scenario_path[1:depth], - algo_params.sigma[node_index], - algo_params.infiltrate_state, - ) - end - - #@infiltrate - - # SOLVE NON-REGULARIZED PROBLEM - ######################################################################## - # Solve the subproblem, note that `require_duals = false`. - TimerOutputs.@timeit DynamicSDDiP "solve_sigma_test" begin - non_reg_results = solve_subproblem_sigma_test( - model, - node, - incoming_state_value, # only values, no State struct! - noise, - scenario_path[1:depth], - algo_params.sigma[node_index], - algo_params.infiltrate_state, - ) - end - - # COMPARE SOLUTIONS - ######################################################################## - if !isapprox(reg_results.objective, non_reg_results.objective) - # if stage objectives are not approximately equal, then the - # regularization is not exact and sigma should be increased - algo_params.sigma[node_index] = algo_params.sigma[node_index] * algo_params.sigma_factor - # marker if new inner loop iteration should be started - # instead of heading to outer loop - sigma_increased = true - end - - # Cumulate the stage_objective. (NOTE: not really required anymore) - cumulative_value += non_reg_results.stage_objective - # Set the outgoing state value as the incoming state value for the next - # node. - # NOTE: We use the states determined by the regularized problem here - #incoming_state_value = copy(subproblem_results.state) - incoming_state_value = copy(reg_results.state) - # Add the outgoing state variable to the list of states we have sampled - # on this forward pass. - push!(sampled_states, incoming_state_value) - - end - - # ===== End: drop off starting state if terminated due to cycle ===== - return ( - scenario_path = scenario_path, - sampled_states = sampled_states, - objective_states = objective_states, - belief_states = belief_states, - cumulative_value = cumulative_value, - sigma_increased = sigma_increased, - ) -end - - -""" -Solving the subproblems for the sigma test. -""" -function solve_subproblem_sigma_test( - model::SDDP.PolicyGraph{T}, - node::SDDP.Node{T}, - state::Dict{Symbol,Float64}, - noise, - scenario_path::Vector{Tuple{T,S}}, - sigma::Float64, - infiltrate_state::Symbol; -) where {T,S} - - # MODEL PARAMETRIZATION - ############################################################################ - subproblem = node.subproblem - - # Parameterize the model. First, fix the value of the incoming state - # variables. Then parameterize the model depending on `noise`. Finally, - # set the objective. - set_incoming_state(node, state) - parameterize(node, noise) - - # SOLUTION - ############################################################################ - JuMP.optimize!(subproblem) - - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - - # NOTE: TO-DO: Attempt numeric recovery as in SDDP - - state = get_outgoing_state(node) - objective = JuMP.objective_value(subproblem) - stage_objective = objective - JuMP.value(bellman_term(node.bellman_function)) - - dual_values = Dict{Symbol,Float64}() - - return ( - state = state, - duals = dual_values, - objective = objective, - stage_objective = stage_objective, - ) -end diff --git a/src/problemModifications.jl b/src/binarization.jl similarity index 56% rename from src/problemModifications.jl rename to src/binarization.jl index 644a0ff8..908e88ad 100644 --- a/src/problemModifications.jl +++ b/src/binarization.jl @@ -12,102 +12,6 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ - -""" -Modifying the forward pass problem to include a regularization term. -""" -function regularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64) - - #NOTE: The copy constraint is not modeled explicitly here. Instead, - # the state variable is unfixed and takes the role of z in our paper. - # It is then subtracted from the fixed value to obtain the so called slack. - - reg_data = node.ext[:regularization_data] - reg_data[:fixed_state_value] = Dict{Symbol,Float64}() - reg_data[:slacks] = Any[] - reg_data[:reg_variables] = JuMP.VariableRef[] - reg_data[:reg_constraints] = JuMP.ConstraintRef[] - - number_of_states = 0 - - # UNFIX THE STATE VARIABLES - ############################################################################ - for (i, (name, state_comp)) in enumerate(node.states) - reg_data[:fixed_state_value][name] = JuMP.fix_value(state_comp.in) - push!(reg_data[:slacks], reg_data[:fixed_state_value][name] - state_comp.in) - JuMP.unfix(state_comp.in) - follow_state_unfixing!(state_comp) - number_of_states = i - end - - # STORE ORIGINAL OBJECTIVE FUNCTION - ############################################################################ - old_obj = reg_data[:old_objective] = JuMP.objective_function(subproblem) - - # DEFINE NEW VARIABLES, CONSTRAINTS AND OBJECTIVE - ############################################################################ - # These variables and constraints are used to define the norm of the slack as a MILP - # Using the lifting approach without binary requirements - slack = reg_data[:slacks] - - # Variable for objective - v = JuMP.@variable(subproblem, base_name = "reg_v") - push!(reg_data[:reg_variables], v) - - # Get sign for regularization term - fact = (JuMP.objective_sense(subproblem) == JuMP.MOI.MIN_SENSE ? 1 : -1) - - # New objective - new_obj = old_obj + fact * sigma * v - JuMP.set_objective_function(subproblem, new_obj) - - # Variables - alpha = JuMP.@variable(subproblem, [i=1:number_of_states], base_name = "alpha") - append!(reg_data[:reg_variables], alpha) - - # Constraints - const_plus = JuMP.@constraint(subproblem, [i=1:number_of_states], -alpha[i] <= slack[i]) - const_minus = JuMP.@constraint(subproblem, [i=1:number_of_states], slack[i] <= alpha[i]) - append!(reg_data[:reg_constraints], const_plus) - append!(reg_data[:reg_constraints], const_minus) - - const_norm = JuMP.@constraint(subproblem, v >= sum(alpha[i] for i in 1:number_of_states)) - push!(reg_data[:reg_constraints], const_norm) - -end - -""" -Modifying the forward pass problem to remove the regularization term -and regain the original model. -""" -function deregularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model) - - reg_data = node.ext[:regularization_data] - - # FIX THE STATE VARIABLES - ############################################################################ - for (i, (name, state_comp)) in enumerate(node.states) - prepare_state_fixing!(node, state_comp) - JuMP.fix(state_comp.in, reg_data[:fixed_state_value][name], force=true) - end - - # REPLACE THE NEW BY THE OLD OBJECTIVE - ############################################################################ - JuMP.set_objective_function(subproblem, reg_data[:old_objective]) - - # DELETE ALL REGULARIZATION-BASED VARIABLES AND CONSTRAINTS - ############################################################################ - delete(subproblem, reg_data[:reg_variables]) - - for constraint in reg_data[:reg_constraints] - delete(subproblem, constraint) - end - - delete!(node.ext, :regularization_data) - -end - - """ Modifying the backward pass problem to include a binary expansion of the state. """ @@ -417,185 +321,3 @@ function determine_anchor_states( end return approx_state_value, fixed_binary_values end - - -""" -Introducing a regularizing term to the backward pass problem in binary space. -""" -function regularize_backward!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64) - - bw_data = node.ext[:backward_data] - binary_states = bw_data[:bin_states] - - number_of_states = size(collect(values(binary_states)), 1) - - reg_data = node.ext[:regularization_data] - reg_data[:fixed_state_value] = Dict{Symbol,Float64}() - reg_data[:slacks] = Any[] - reg_data[:reg_variables] = JuMP.VariableRef[] - reg_data[:reg_constraints] = JuMP.ConstraintRef[] - - # DETERMINE SIGMA TO BE USED IN BINARY SPACE - ############################################################################ - Umax = 0 - for (i, (name, state_comp)) in enumerate(node.states) - if state_comp.info.out.upper_bound > Umax - Umax = state_comp.info.out.upper_bound - end - end - # Here, not sigma, but a different regularization parameter is used - sigma_bin = sigma * Umax - - # UNFIX THE STATE VARIABLES - ############################################################################ - for (i, (name, state_comp)) in enumerate(binary_states) - reg_data[:fixed_state_value][name] = JuMP.fix_value(state_comp) - push!(reg_data[:slacks], reg_data[:fixed_state_value][name] - state_comp) - JuMP.unfix(state_comp) - follow_state_unfixing_binary!(state_comp) - end - - # STORE ORIGINAL OBJECTIVE FUNCTION - ############################################################################ - old_obj = reg_data[:old_objective] = JuMP.objective_function(subproblem) - - # DEFINE NEW VARIABLES, CONSTRAINTS AND OBJECTIVE - ############################################################################ - # These variables and constraints are used to define the norm of the slack as a MILP - # Using the lifting approach without binary requirements - slack = reg_data[:slacks] - - # Variable for objective - v = JuMP.@variable(subproblem, base_name = "reg_v") - push!(reg_data[:reg_variables], v) - - # Get sign for regularization term - fact = (JuMP.objective_sense(subproblem) == JuMP.MOI.MIN_SENSE ? 1 : -1) - - # New objective - new_obj = old_obj + fact * sigma_bin * v - JuMP.set_objective_function(subproblem, new_obj) - - # Variables - alpha = JuMP.@variable(subproblem, [i=1:number_of_states], base_name = "alpha") - append!(reg_data[:reg_variables], alpha) - - # Constraints - const_plus = JuMP.@constraint(subproblem, [i=1:number_of_states], -alpha[i] <= slack[i]) - const_minus = JuMP.@constraint(subproblem, [i=1:number_of_states], slack[i] <= alpha[i]) - append!(reg_data[:reg_constraints], const_plus) - append!(reg_data[:reg_constraints], const_minus) - - const_norm = JuMP.@constraint(subproblem, v >= sum(alpha[i] for i in 1:number_of_states)) - push!(reg_data[:reg_constraints], const_norm) - -end - - -""" -Regaining the unregularized problem in binary space. -""" -function deregularize_backward!(node::SDDP.Node, subproblem::JuMP.Model) - - reg_data = node.ext[:regularization_data] - bw_data = node.ext[:backward_data] - - # FIX THE STATE VARIABLES - ############################################################################ - for (i, (name, state_comp)) in enumerate(bw_data[:bin_states]) - prepare_state_fixing_binary!(node, state_comp) - JuMP.fix(state_comp, reg_data[:fixed_state_value][name], force=true) - end - - # REPLACE THE NEW BY THE OLD OBJECTIVE - ############################################################################ - JuMP.set_objective_function(subproblem, reg_data[:old_objective]) - - # DELETE ALL REGULARIZATION-BASED VARIABLES AND CONSTRAINTS - ############################################################################ - delete(subproblem, reg_data[:reg_variables]) - - for constraint in reg_data[:reg_constraints] - delete(subproblem, constraint) - end - - delete!(node.ext, :regularization_data) - -end - - -""" -Check if an increase of the number of binary variables is required. -""" -function binary_refinement_check( - model::SDDP.PolicyGraph{T}, - previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, - sampled_states::Vector{Dict{Symbol,Float64}}, - solution_check::Bool, - ) where {T} - - # Check if feasible solution has changed since last iteration - # If not, then the cut was not tight enough, so binary approximation should be refined - for i in 1:size(previous_solution,1) - for (name, state_comp) in model.nodes[i].states - current_sol = sampled_states[i][name] - previous_sol = previous_solution[i][name] - if ! isapprox(current_sol, previous_sol) - solution_check = false - end - end - end - - return solution_check - -end - - -""" -Executing a binary refinement: Increasing the number of binary variables to -approximate the states -""" -function binary_refinement( - model::SDDP.PolicyGraph{T}, - algo_params::NCNBD.AlgoParams, - binary_refinement::Symbol, - ) where {T} - - all_refined = Int[] - - # Consider stage 2 here (should be the same for all following stages) - # Precision is only used (and increased) for continuous variables - for (name, state_comp) in model.nodes[2].states - if !state_comp.info.in.binary && !state_comp.info.in.integer - current_prec = algo_params.binary_precision[name] - ub = state_comp.info.in.upper_bound - K = SDDP._bitsrequired(round(Int, ub / current_prec)) - new_prec = ub / sum(2^(k-1) for k in 1:K+1) - - # Only apply refinement if int64 is appropriate to represent this number - if ub / new_prec > 2^63 - 1 - push!(all_refined, 0) - else - push!(all_refined, 1) - algo_params.binary_precision[name] = new_prec - end - - else - push!(all_refined, 2) - end - end - - # Check refinement status - if 0 in all_refined - if 1 in all_refined - binary_refinement = :partial - else - binary_refinement = :impossible - end - else - binary_refinement = :all - end - - return binary_refinement - -end diff --git a/src/binaryRefinement.jl b/src/binaryRefinement.jl new file mode 100644 index 00000000..d5d2209f --- /dev/null +++ b/src/binaryRefinement.jl @@ -0,0 +1,107 @@ +# Copyright (c) 2021 Christian Fuellner + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Check if an increase of the number of binary variables is required +if binary approximation is used. +""" +function binary_refinement_check( + model::SDDP.PolicyGraph{T}, + previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, + sampled_states::Vector{Dict{Symbol,Float64}}, + refinement_check::Bool, + #bound_check::Bool, + state_approximation_regime::DynamicSDDiP.BinaryApproximation, + ) where {T} + + solution_check = true + + # Check if feasible solution has changed since last iteration + # If not, then the cut was not tight enough, so binary approximation should be refined + for i in 1:size(previous_solution,1) + for (name, state_comp) in model.nodes[i].states + current_sol = sampled_states[i][name] + previous_sol = previous_solution[i][name] + if ! isapprox(current_sol, previous_sol) + solution_check = false + end + end + end + + #if solution_check && bound_check + # # Binary precision should be increased + # refinement_check = true + #end + + refinement_check = solution_check + + return refinement_check +end + +""" +Check if an increase of the number of binary variables is required +if no binary approximation is used. +""" +function binary_refinement_check( + model::SDDP.PolicyGraph{T}, + previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, + sampled_states::Vector{Dict{Symbol,Float64}}, + refinement_check::Bool, + #bound_check::Bool, + state_approximation_regime::DynamicSDDiP.NoStateApproximation, + ) where {T} + + return refinement_check +end + +""" +Executing a binary refinement: Increasing the number of binary variables to +approximate the states +""" +function binary_refinement( + model::SDDP.PolicyGraph{T}, + binary_precision::Dict{Symbol, Float64}, + binary_refinement::Symbol, + ) where {T} + + all_refined = Int[] + + # Consider stage 2 here (should be the same for all following stages) + # Precision is only used (and increased) for continuous variables + for (name, state_comp) in model.nodes[2].states + if !state_comp.info.in.binary && !state_comp.info.in.integer + current_prec = binary_precision[name] + ub = state_comp.info.in.upper_bound + K = SDDP._bitsrequired(round(Int, ub / current_prec)) + new_prec = ub / sum(2^(k-1) for k in 1:K+1) + + # Only apply refinement if int64 is appropriate to represent this number + if ub / new_prec > 2^63 - 1 + push!(all_refined, 0) + else + push!(all_refined, 1) + binary_precision[name] = new_prec + end + + else + push!(all_refined, 2) + end + end + + # Check refinement status + if 0 in all_refined + if 1 in all_refined + binary_refinement = :partial + else + binary_refinement = :impossible + end + else + binary_refinement = :all + end + + return binary_refinement + +end diff --git a/src/forwardPass.jl b/src/forwardPass.jl new file mode 100644 index 00000000..75745c3b --- /dev/null +++ b/src/forwardPass.jl @@ -0,0 +1,154 @@ +# The functions +# > "forward_pass", +# > "solve_subproblem_forward", +# are derived from similar named functions (forward_pass, solve_subproblem) +# in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Executing the forward pass of an inner loop iteration for DynamicSDDiP. +""" +function forward_pass(model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, + algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, + ::SDDP.DefaultForwardPass) where {T} + + ############################################################################ + # SAMPLING AND INITIALIZATION (SIMLAR TO SDDP) + ############################################################################ + # First up, sample a scenario. Note that if a cycle is detected, this will + # return the cycle node as well. + scenario_path, terminated_due_to_cycle = SDDP.sample_scenario(model, algo_params.sampling_scheme) + + # Storage for the list of outgoing states that we visit on the forward pass. + sampled_states = Dict{Symbol,Float64}[] + # Our initial incoming state. + incoming_state_value = copy(model.initial_root_state) + # A cumulator for the stage-objectives. + cumulative_value = 0.0 + + ############################################################################ + # ACTUAL ITERATION + ######################################################################## + # Iterate down the scenario tree. + for (depth, (node_index, noise)) in enumerate(scenario_path) + node = model[node_index] + + ######################################################################## + # SET SOLVER + ######################################################################## + DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + + ######################################################################## + # SUBPROBLEM SOLUTION + ######################################################################## + # Solve the subproblem, note that `require_duals = false`. + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_FP" begin + subproblem_results = solve_subproblem_forward( + model, + node, + node_index, + incoming_state_value, # only values, no State struct! + noise, + scenario_path[1:depth], + algo_params.regularization_regime, + ) + end + # Cumulate the stage_objective. + cumulative_value += subproblem_results.stage_objective + # Set the outgoing state value as the incoming state value for the next + # node. + incoming_state_value = copy(subproblem_results.state) + # Add the outgoing state variable to the list of states we have sampled + # on this forward pass. + push!(sampled_states, incoming_state_value) + + end + + return ( + scenario_path = scenario_path, + sampled_states = sampled_states, + objective_states = objective_states, + belief_states = belief_states, + cumulative_value = cumulative_value, + ) +end + + +""" +Solving the subproblem within the forward pass of a loop of DynamicSDDiP. +""" +# Internal function: solve the subproblem associated with node given the +# incoming state variables state and realization of the stagewise-independent +# noise term noise. If require_duals=true, also return the dual variables +# associated with the fixed constraint of the incoming state variables. +function solve_subproblem_forward( + model::SDDP.PolicyGraph{T}, + node::SDDP.Node{T}, + node_index::Int64, + state::Dict{Symbol,Float64}, + noise, + scenario_path::Vector{Tuple{T,S}}, + infiltrate_state::Symbol, + regularization_regime::DynamicSDDiP.AbstractRegularizationRegime; +) where {T,S} + + subproblem = node.subproblem + + ############################################################################ + # MODEL PARAMETRIZATION + ############################################################################ + # Parameterize the model. First, fix the value of the incoming state + # variables. Then parameterize the model depending on `noise`. Finally, + # set the objective. + set_incoming_state(node, state) + parameterize(node, noise) + + ############################################################################ + # REGULARIZE SUBPROBLEM IF REQUIRED + ############################################################################ + if node_index > 1 + node.ext[:regularization_data] = Dict{Symbol,Any}() + regularize_subproblem!(node, node_index, subproblem, regularization_regime) + end + + ############################################################################ + # SOLUTION + ############################################################################ + @infiltrate infiltrate_state in [:all] + JuMP.optimize!(subproblem) + + if haskey(model.ext, :total_solves) + model.ext[:total_solves] += 1 + else + model.ext[:total_solves] = 1 + end + + # Maybe attempt numerical recovery as in SDDP + + state = get_outgoing_state(node) + objective = JuMP.objective_value(subproblem) + stage_objective = objective - JuMP.value(bellman_term(node.bellman_function)) + @infiltrate infiltrate_state in [:all] + + ############################################################################ + # DE-REGULARIZE SUBPROBLEM IF REQUIRED + ############################################################################ + if node_index > 1 + deregularize_subproblem!(node, subproblem, regularization_regime) + end + + return ( + state = state, + objective = objective, + stage_objective = stage_objective, + ) +end diff --git a/src/regularizations.jl b/src/regularizations.jl index 8aa8399f..f484e055 100644 --- a/src/regularizations.jl +++ b/src/regularizations.jl @@ -6,11 +6,13 @@ """ -Modifying the forward pass problem to include a regularization term. +Modifying the forward pass problem to include a regularization term +if regularization is used. """ -function regularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64) +function regularize_subproblem!(node::SDDP.Node, node_index::Int64, + subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.Regularization) - #NOTE: The copy constraint is not modeled explicitly here. Instead, + # Note that the copy constraint is not modeled explicitly here. Instead, # the state variable is unfixed and takes the role of z in our paper. # It is then subtracted from the fixed value to obtain the so called slack. @@ -50,7 +52,7 @@ function regularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, sigma:: fact = (JuMP.objective_sense(subproblem) == JuMP.MOI.MIN_SENSE ? 1 : -1) # New objective - new_obj = old_obj + fact * sigma * v + new_obj = old_obj + fact * regularization_regime.sigma[node_index] * v JuMP.set_objective_function(subproblem, new_obj) # Variables @@ -68,11 +70,20 @@ function regularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, sigma:: end +""" +Trivial modification of the forward pass problem if no regularization is used. +""" +function regularize_subproblem!(node::SDDP.Node, node_index::Int64, + subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.NoRegularization) + return +end + + """ Modifying the forward pass problem to remove the regularization term -and regain the original model. +and regain the original model if regularization is used. """ -function deregularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model) +function deregularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.Regularization) reg_data = node.ext[:regularization_data] @@ -99,6 +110,13 @@ function deregularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model) end +""" +Trivial modification of the forward pass problem if no regularization is used. +""" +function deregularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.NoRegularization) + return +end + """ Introducing a regularizing term to the backward pass problem in binary space. @@ -203,80 +221,3 @@ function deregularize_backward!(node::SDDP.Node, subproblem::JuMP.Model) delete!(node.ext, :regularization_data) end - - -""" -Check if an increase of the number of binary variables is required. -""" -function binary_refinement_check( - model::SDDP.PolicyGraph{T}, - previous_solution::Union{Vector{Dict{Symbol,Float64}},Nothing}, - sampled_states::Vector{Dict{Symbol,Float64}}, - solution_check::Bool, - ) where {T} - - # Check if feasible solution has changed since last iteration - # If not, then the cut was not tight enough, so binary approximation should be refined - for i in 1:size(previous_solution,1) - for (name, state_comp) in model.nodes[i].states - current_sol = sampled_states[i][name] - previous_sol = previous_solution[i][name] - if ! isapprox(current_sol, previous_sol) - solution_check = false - end - end - end - - return solution_check - -end - - -""" -Executing a binary refinement: Increasing the number of binary variables to -approximate the states -""" -function binary_refinement( - model::SDDP.PolicyGraph{T}, - algo_params::NCNBD.AlgoParams, - binary_refinement::Symbol, - ) where {T} - - all_refined = Int[] - - # Consider stage 2 here (should be the same for all following stages) - # Precision is only used (and increased) for continuous variables - for (name, state_comp) in model.nodes[2].states - if !state_comp.info.in.binary && !state_comp.info.in.integer - current_prec = algo_params.binary_precision[name] - ub = state_comp.info.in.upper_bound - K = SDDP._bitsrequired(round(Int, ub / current_prec)) - new_prec = ub / sum(2^(k-1) for k in 1:K+1) - - # Only apply refinement if int64 is appropriate to represent this number - if ub / new_prec > 2^63 - 1 - push!(all_refined, 0) - else - push!(all_refined, 1) - algo_params.binary_precision[name] = new_prec - end - - else - push!(all_refined, 2) - end - end - - # Check refinement status - if 0 in all_refined - if 1 in all_refined - binary_refinement = :partial - else - binary_refinement = :impossible - end - else - binary_refinement = :all - end - - return binary_refinement - -end diff --git a/src/sigmaTest.jl b/src/sigmaTest.jl new file mode 100644 index 00000000..ad921c4c --- /dev/null +++ b/src/sigmaTest.jl @@ -0,0 +1,118 @@ +# The functions +# > "forward_sigma_test" +# > "solve_subproblem_sigma_test" +# are derived from similar named functions (forward_pass, solve_subproblem) +# in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Executing a forward pass to check if parameter sigma is chosen sufficiently +large in DynamicSDDiP. +""" +function forward_sigma_test( + model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, + algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, + scenario_path::Vector{Tuple{T,S}}, sigma_increased::Bool) where {T,S} + + # INITIALIZATION (NO SAMPLING HERE!) + ############################################################################ + # Storage for the list of outgoing states that we visit on the forward pass. + sampled_states = Dict{Symbol,Float64}[] + # Our initial incoming state. + incoming_state_value = copy(model.ext[:lin_initial_root_state]) + # A cumulator for the stage-objectives. + cumulative_value = 0.0 + + # ACTUAL ITERATION + ######################################################################## + # Iterate down the scenario tree. + for (depth, (node_index, noise)) in enumerate(scenario_path) + node = model[node_index] + + # SET SOLVER + ######################################################################## + DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + + # SOLVE REGULARIZED PROBLEM + ############################################################################ + # This has to be done in this sigma test again, since the current upper bound + # may not be the best upper bound and thus, just comparing the bounds is not + # sufficient. Moreover, we do it in this loop to not increase sigma for + # all stages, but only where it is needed + + # Solve the subproblem, note that `require_duals = false`. + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_sigma_test_reg" begin + reg_results = solve_subproblem_forward( + model, + node, + node_index, + incoming_state_value, # only values, no State struct! + noise, + scenario_path[1:depth], + algo_params.infiltrate_state + algo_params.regularization_regime, + ) + end + + #@infiltrate + + # SOLVE NON-REGULARIZED PROBLEM + ######################################################################## + # Solve the subproblem, note that `require_duals = false`. + TimerOutputs.@timeit DynamicSDDiP "solve_sigma_test" begin + non_reg_results = solve_subproblem_forward( + model, + node, + node_index, + incoming_state_value, # only values, no State struct! + noise, + scenario_path[1:depth], + algo_params.infiltrate_state + DynamicSDDiP.NoRegularization, + ) + end + + # COMPARE SOLUTIONS + ######################################################################## + if !isapprox(reg_results.objective, non_reg_results.objective) + sigma = algo_params.regularization_regime.sigma[node_index] + sigma_factor = algo_params.regularization_regime.sigma_factor + # if stage objectives are not approximately equal, then the + # regularization is not exact and sigma should be increased + sigma = sigma * sigma_factor + # marker if new inner loop iteration should be started + # instead of heading to outer loop + sigma_increased = true + end + + # Cumulate the stage_objective. (NOTE: not really required anymore) + cumulative_value += non_reg_results.stage_objective + # Set the outgoing state value as the incoming state value for the next node. + # NOTE: We use the states determined by the regularized problem here + #incoming_state_value = copy(subproblem_results.state) + incoming_state_value = copy(reg_results.state) + # Add the outgoing state variable to the list of states we have sampled + # on this forward pass. + push!(sampled_states, incoming_state_value) + + end + + # ===== End: drop off starting state if terminated due to cycle ===== + return ( + scenario_path = scenario_path, + sampled_states = sampled_states, + objective_states = objective_states, + belief_states = belief_states, + cumulative_value = cumulative_value, + sigma_increased = sigma_increased, + ) +end diff --git a/src/solveStarter.jl b/src/solveStarter.jl deleted file mode 100644 index 6aa417ec..00000000 --- a/src/solveStarter.jl +++ /dev/null @@ -1,305 +0,0 @@ -# The functions -# > "solve", -# > "master_loop" -# > "inner_loop" -# are derived from similar named functions (solve, master_loop) in the 'SDDP.jl' package by -# Oscar Dowson and released under the Mozilla Public License 2.0. -# The reproduced function and other functions in this file are also released -# under Mozilla Public License 2.0 - -# Copyright (c) 2021 Christian Fuellner -# Copyright (c) 2021 Oscar Dowson - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Solves the `model`. In contrast to SDDP.jl, here in algo_params specific parameters -configuring the solution procedure are given to the function. -""" -function solve( - model::SDDP.PolicyGraph, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers; - ############################################################################ - iteration_limit::Union{Int,Nothing} = nothing, - time_limit::Union{Real,Nothing} = nothing, - print_level::Int = 1, - log_file::String = "DynamicSDDiP.log", - log_frequency::Int = 1, - run_numerical_stability_report::Bool = true, - stopping_rules = SDDP.AbstractStoppingRule[], - risk_measure = SDDP.Expectation(), - sampling_scheme = SDDP.InSampleMonteCarlo(), - cut_type = SDDP.SINGLE_CUT, - cycle_discretization_delta::Float64 = 0.0, - refine_at_similar_nodes::Bool = true, - cut_deletion_minimum::Int = 1, - backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme = SDDP.CompleteSampler(), - dashboard::Bool = false, - parallel_scheme::SDDP.AbstractParallelScheme = SDDP.Serial(), - forward_pass::SDDP.AbstractForwardPass = SDDP.DefaultForwardPass(), -) - - # INITIALIZATION (AS IN SDDP) - ############################################################################ - # Reset the TimerOutput and define logging-specific variables - TimerOutputs.reset_timer!(DynamicSDDiP_TIMER) - log_file_handle = open(log_file, "a") - log = Log[] - - if print_level > 0 - print_helper(print_banner, log_file_handle) - end - - if print_level > 1 - print_helper(print_parameters, log_file_handle, algo_params, applied_solvers) - end - - # NOTE: Add run_numerical_stability_report as in SDDP? - - # Convert the vector to an AbstractStoppingRule. Otherwise if the user gives - # something like stopping_rules = [SDDP.IterationLimit(100)], the vector - # will be concretely typed and we can't add a TimeLimit. - stopping_rules = convert(Vector{SDDP.AbstractStoppingRule}, stopping_rules) - # Add the limits as stopping rules. An IterationLimit or TimeLimit may - # already exist in stopping_rules, but that doesn't matter. - if iteration_limit !== nothing - push!(stopping_rules, SDDP.IterationLimit(iteration_limit)) - end - if time_limit !== nothing - push!(stopping_rules, SDDP.TimeLimit(time_limit)) - end - if length(stopping_rules) == 0 - @warn( - "You haven't specified a stopping rule! You can only terminate " * - "the call to DynamicSDDiP.solve via a keyboard interrupt ([CTRL+C])." - ) - end - - # Update the nodes with the selected cut type (SINGLE_CUT or MULTI_CUT) - # and the cut deletion minimum. - if cut_deletion_minimum < 0 - cut_deletion_minimum = typemax(Int) - end - for (key, node) in model.nodes - node.bellman_function.cut_type = cut_type - node.bellman_function.global_theta.cut_oracle.deletion_minimum = - cut_deletion_minimum - for oracle in node.bellman_function.local_thetas - oracle.cut_oracle.deletion_minimum = cut_deletion_minimum - end - end - - dashboard_callback = if dashboard - launch_dashboard() - else - (::Any, ::Any) -> nothing - end - - SDDP_options = DynamicSDDiP.Options( - model, - model.initial_root_state, - sampling_scheme, - backward_sampling_scheme, - risk_measure, - cycle_discretization_delta, - refine_at_similar_nodes, - stopping_rules, - dashboard_callback, - print_level, - time(), - log, - log_file_handle, - log_frequency, - forward_pass, - ) - - # MODEL START - ############################################################################ - status = :not_solved - try - status = solve_DynamicSDDiP(parallel_scheme, model, sddp_options, algo_params, applied_solvers) - catch ex - if isa(ex, InterruptException) - status = :interrupted - interrupt(parallel_scheme) - else - close(log_file_handle) - rethrow(ex) - end - finally - end - - # lOG MODEL RESULTS - ############################################################################ - results = DynamicSDDiP.Results(status, log) - model.ext[:results] = results - if print_level > 0 - print_helper(print_footer, log_file_handle, results) - if print_level > 1 - print_helper(TimerOutputs.print_timer, log_file_handle, DynamicSDDiP_TIMER) - print_helper(println, log_file_handle) - end - end - close(log_file_handle) - return -end - -""" -Solves the `model` using DynamicSDDiP in a serial scheme. -""" - -function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, - options::DynamicSDDiP.Options, algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers) where {T} - - # SET UP SOME STUFF - ############################################################################ - for (node_index, children) in model.nodes - node = model.nodes[node_index] - - # Set info for x_in (taking bounds, binary, integer info from previous stage's x_out) - #----------------------------------------------------------------------- - if node_index > 1 - for (i, (name, state)) in enumerate(node.states) - # Get correct state_info - state_info = model.nodes[node_index-1].states[name].info.out - - if state_info.has_lb - JuMP.set_lower_bound(state.in, state_info.lower_bound) - end - if state_info.has_ub - JuMP.set_upper_bound(state.in, state_info.upper_bound) - end - if state_info.binary - JuMP.set_binary(state.in) - elseif state_info.integer - JuMP.set_integer(state.in) - end - - # Store info to reset it later - state.info.in = state_info - end - end - - # also re-initialize the existing value function such that nonlinear cuts are used - # fortunately, node.bellman_function requires no specific type - node.bellman_function = initialize_bellman_function_nonconvex(bellman_function, model, node) - node.bellman_function.cut_type = cut_type - node.bellman_function.global_theta.cut_oracle.deletion_minimum = deletion_minimum - for oracle in node.bellman_function.local_thetas - oracle.cut_oracle.deletion_minimum = deletion_minimum - end - end - - @infiltrate algo_params.infiltrate_state == :all - - # CALL ACTUAL SOLUTION PROCEDURE - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "loop" begin - status = master_loop(parallel_scheme, model, options, algo_params, applied_solvers) - end - return status - -end - - -""" -Loop function of DynamicSDDiP. -""" - -function master_loop(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, - options::DynamicSDDiP.Options, algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers) where {T} - - # INITIALIZE PARAMETERS REQUIRED FOR REFINEMENTS - ############################################################################ - previous_solution = nothing - previous_bound = nothing - sigma_increased = false - bound_check = false - - # INITIALIZE BEST KNOWN POINT AND OBJECTIVE VALUE - ############################################################################ - model.ext[:best_objective] = model.objective_sense == JuMP.MOI.MIN_SENSE ? Inf : -Inf - model.ext[:best_point] = Vector{Dict{Symbol,Float64}}() - - # ACTUAL LOOP - ############################################################################ - while true - # start an iteration - TimerOutputs.@timeit DynamicSDDiP_TIMER "iterations" begin - result = iteration(model, options, algo_params, applied_solvers, previous_solution, bound_check, sigma_increased) - end - - # set previous_solution and previous_bound using iteration results - previous_solution = result.current_sol - previous_bound = result.lower_bound - - # logging - log_iteration(options, options.log) - - # initialize sigma_increased - sigma_increased = false - - # initialize bound_check - bound_check = true - @infiltrate algo_params.infiltrate_state in [:all, :sigma] - - # IF CONVERGENCE IS ACHIEVED, CHECK IF ACTUAL OR ONLY REGULARIZED PROBLEM IS SOLVED - ######################################################################## - if result.has_converged && algo_params.regularization - TimerOutputs.@timeit DynamicSDDiP_TIMER "sigma_test" begin - sigma_test_results = forward_sigma_test(model, options, algo_params, applied_solvers, result.scenario_path, options.forward_pass, sigma_increased) - end - sigma_increased = sigma_test_results.sigma_increased - @infiltrate algo_params.infiltrate_state in [:all, :sigma] - - if sigma_increased - # reset previous values, as model is changed and convergence not achieved - previous_solution = nothing - previous_bound = nothing - # binary refinement only when no sigma refinement has been made - bound_check = false - else - # return convergence status - return result.status - end - - # IF NO CONVERGENCE IS ACHIEVED, DO DIFFERENT CHECKS - ######################################################################## - else - # CHECK IF LOWER BOUND HAS IMPROVED - ############################################################################ - # NOTE: If not, then the cut was (probably) not tight enough, - # so the binary approximation should be refined in the next iteration. - # As different trial states could yield the same lower bound, this - # is only done if also the trial state does not change, though. - if !isnothing(previous_bound) - if !isapprox(previous_bound, result.lower_bound) - bound_check = false - end - else - bound_check = false - end - - # CHECK IF SIGMA SHOULD BE INCREASED (DUE TO LB > UB) - ############################################################################ - if result.upper_bound - result.lower_bound < - algo_params.opt_atol * 0.1 - if algo_params.regularization - algo_params.sigma = algo_params.sigma * algo_params.sigma_factor - sigma_increased = true - previous_solution = nothing - previous_bound = nothing - bound_check = false - else - error("LB < UB for DynamicSDDiP. Terminating.") - else - sigma_increased = false - end - - end - end -end From 2439bda3f9d6bc3a7f2dcc3eba11caa6669f806d Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 8 Oct 2021 15:08:13 -0400 Subject: [PATCH 08/30] Changing first part of backward pass and binarization to new abstract types --- src/backwardPass.jl | 201 ++++++++++++++++++-------------------- src/binarization.jl | 231 ++++++++++++++++++++++++++------------------ 2 files changed, 230 insertions(+), 202 deletions(-) diff --git a/src/backwardPass.jl b/src/backwardPass.jl index a50374ec..d72224f0 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -31,89 +31,94 @@ function backward_pass( objective_states::Vector{NTuple{N,Float64}}, belief_states::Vector{Tuple{Int,Dict{T,Float64}}}) where {T,NoiseType,N} + #################################################################### + # INITIALIZATION + #################################################################### + + # storage for cuts cuts = Dict{T,Vector{Any}}(index => Any[] for index in keys(model.nodes)) + # storage for data on solving Lagrangian dual model.ext[:lag_iterations] = Int[] model.ext[:lag_status] = Symbol[] + ############################################################################ + # Traverse backwards through the stages + ############################################################################ for index = length(scenario_path):-1:1 outgoing_state = sampled_states[index] - objective_state = get(objective_states, index, nothing) - partition_index, belief_state = get(belief_states, index, (0, nothing)) items = BackwardPassItems(T, SDDP.Noise) - if belief_state !== nothing - # NOTE: SDDP: Update the cost-to-go function for partially observable model. - else - node_index, _ = scenario_path[index] - node = model[node_index] - if length(node.children) == 0 - continue - end - # Dict to store values of binary approximation of the state - # Note that we could also retrieve this from the actual trial point - # (outgoing_state) or from its approximation via binexpand. However, - # this collection is not only important to obtain the correct values, - # but also to store them together with the symbol/name of the variable. - node.ext[:binary_state_values] = Dict{Symbol, Float64}() + node_index, _ = scenario_path[index] + node = model[node_index] + if length(node.children) == 0 + continue + end - # SOLVE ALL CHILDREN PROBLEM - #################################################################### - solve_all_children( + # Dict to store values of binary approximation of the state + # Note that we could also retrieve this from the actual trial point + # (outgoing_state) or from its approximation via binexpand. However, + # this collection is not only important to obtain the correct values, + # but also to store them together with the symbol/name of the variable. + node.ext[:binary_state_values] = Dict{Symbol, Float64}() + + #################################################################### + # SOLVE ALL CHILDREN PROBLEMS + #################################################################### + solve_all_children( + model, + node, + node_index, + items, + 1.0, + belief_state, + objective_state, + outgoing_state, + algo_params.backward_sampling_scheme, + scenario_path[1:index], + algo_params, + applied_solvers + ) + + # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS + #################################################################### + anchor_points = Dict{Symbol,Float64}() + for (name, value) in outgoing_state + state_comp = node.states[name] + epsilon = algo_params.binary_precision[name] + (approx_state_value, ) = determine_anchor_states(state_comp, value, epsilon) + anchor_points[name] = approx_state_value + end + + @infiltrate algo_params.infiltrate_state in [:all] + + # REFINE BELLMAN FUNCTION BY ADDING CUTS + #################################################################### + TimerOutputs.@timeit DynamicSDDiP_TIMER "update_bellman" begin + new_cuts = refine_bellman_function( model, node, node_index, - items, - 1.0, - belief_state, - objective_state, + node.bellman_function, + options.risk_measures[node_index], outgoing_state, - options.backward_sampling_scheme, - scenario_path[1:index], + anchor_points, + items.bin_state, + items.duals, + items.supports, + items.probability, + items.objectives, algo_params, applied_solvers ) + end + push!(cuts[node_index], new_cuts) + #NOTE: Has to be adapted for stochastic case + push!(model.ext[:lag_iterations], sum(items.lag_iterations)) + push!(model.ext[:lag_status], items.lag_status[1]) - # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS - #################################################################### - anchor_points = Dict{Symbol,Float64}() - for (name, value) in outgoing_state - state_comp = node.states[name] - epsilon = algo_params.binary_precision[name] - (approx_state_value, ) = determine_anchor_states(state_comp, value, epsilon) - anchor_points[name] = approx_state_value - end - - @infiltrate algo_params.infiltrate_state in [:all] - - # REFINE BELLMAN FUNCTION BY ADDING CUTS - #################################################################### - TimerOutputs.@timeit DynamicSDDiP_TIMER "update_bellman" begin - new_cuts = refine_bellman_function( - model, - node, - node_index, - node.bellman_function, - options.risk_measures[node_index], - outgoing_state, - anchor_points, - items.bin_state, - items.duals, - items.supports, - items.probability, - items.objectives, - algo_params, - applied_solvers - ) - end - push!(cuts[node_index], new_cuts) - push!(model.ext[:lag_iterations], sum(items.lag_iterations)) - #NOTE: Has to be adapted for stochastic case - push!(model.ext[:lag_status], items.lag_status[1]) - - #TODO: Implement cut-sharing as in SDDP + #TODO: Implement cut-sharing as in SDDP - end end return cuts end @@ -148,6 +153,9 @@ function solve_all_children( else scenario_path[end] = (child.term, noise.term) end + #################################################################### + # IF SOLUTIONS FOR THIS NODE ARE CACHED ALREADY, USE THEM + #################################################################### if haskey(items.cached_solutions, (child.term, noise.term)) sol_index = items.cached_solutions[(child.term, noise.term)] push!(items.duals, items.duals[sol_index]) @@ -160,23 +168,9 @@ function solve_all_children( push!(items.lag_iterations, items.lag_iterations[sol_index]) push!(items.lag_status, items.lag_status[sol_index]) else - # Update belief state, etc. - if belief_state !== nothing - current_belief = child_node.belief_state::SDDP.BeliefState{T} - current_belief.updater( - current_belief.belief, - belief_state, - current_belief.partition_index, - noise.term, - ) - end - if objective_state !== nothing - SDDP.update_objective_state( - child_node.objective_state, - objective_state, - noise.term, - ) - end + ################################################################ + # SOLVE THE BACKWARD PASS PROBLEM + ################################################################ TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_BP" begin subproblem_results = solve_subproblem_backward( model, @@ -229,29 +223,29 @@ function solve_subproblem_backward( applied_solvers::DynamicSDDiP.AppliedSolvers; ) where {T,S} + ############################################################################ # MODEL PARAMETRIZATION ############################################################################ subproblem = node.subproblem - # storage for backward pass data + # Storage for backward pass data node.ext[:backward_data] = Dict{Symbol,Any}() - # Parameterize the model. First, fix the value of the incoming state - # variables. Then parameterize the model depending on `noise`. Finally, - # set the objective. + # Parameterize the model. Fix the value of the incoming state variables. + # Then parameterize the model depending on `noise` and set the objective. set_incoming_state(node, state) parameterize(node, noise) @infiltrate algo_params.infiltrate_state in [:all] - # CHANGE TO BINARY SPACE FOR STATE VARIABLES ############################################################################ - if algo_params.binary_approx - TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin - changeToBinarySpace!(node, subproblem, state, algo_params.binary_precision) - end + # CHANGE STATE SPACE IF BINARY APPROXIMATION IS USED + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin + changeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) end + ############################################################################ # INITIALIZE DUALS ############################################################################ TimerOutputs.@timeit DynamicSDDiP_TIMER "dual_initialization" begin @@ -311,12 +305,11 @@ function solve_subproblem_backward( @infiltrate algo_params.infiltrate_state in [:all] + ############################################################################ # REGAIN ORIGINAL MODEL ############################################################################ - if algo_params.binary_approx - TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin - changeToOriginalSpace!(node, subproblem, state) - end + TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin + rechangeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) end return ( @@ -479,19 +472,6 @@ function calculate_bound( end node = model[child.term] for noise in node.noise_terms - if node.objective_state !== nothing - SDDP.update_objective_state( - node.objective_state, - node.objective_state.initial_value, - noise.term, - ) - end - # Update belief state, etc. - if node.belief_state !== nothing - belief = node.belief_state::SDDP.BeliefState{T} - partition_index = belief.partition_index - belief.updater(belief.belief, current_belief, partition_index, noise.term) - end subproblem_results = solve_first_stage_problem( model, node, @@ -532,7 +512,8 @@ function solve_first_stage_problem( scenario_path::Vector{Tuple{T,S}}; ) where {T,S} - # MODEL PARAMETRIZATION (-> LINEARIZED SUBPROBLEM!) + ############################################################################ + # MODEL PARAMETRIZATION ############################################################################ subproblem = node.subproblem @@ -542,6 +523,7 @@ function solve_first_stage_problem( set_incoming_state(node, state) parameterize(node, noise) + ############################################################################ # SOLUTION ############################################################################ JuMP.optimize!(subproblem) @@ -552,13 +534,14 @@ function solve_first_stage_problem( model.ext[:total_solves] = 1 end - # TODO: Attempt numerical recovery as in SDDP + # Maybe attempt numerical recovery as in SDDP state = get_outgoing_state(node) stage_objective = JuMP.value(node.stage_objective) objective = JuMP.objective_value(subproblem) dual_values = get_dual_variables(node, node.integrality_handler) + ############################################################################ # DETERMINE THE PROBLEM SIZE ############################################################################ problem_size = Dict{Symbol,Int64}() diff --git a/src/binarization.jl b/src/binarization.jl index 908e88ad..ab6b7182 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -13,29 +13,35 @@ ################################################################################ """ -Modifying the backward pass problem to include a binary expansion of the state. +Modifying the backward pass problem to include a binary expansion of the state +if such expansion is used. """ -function changeToBinarySpace!( +function changeStateSpace!( node::SDDP.Node, subproblem::JuMP.Model, state::Dict{Symbol,Float64}, - binary_precision::Dict{Symbol,Float64} + state_approximation_regime::DynamicSDDiP.BinaryApproximation ) - #NOTE: The copy constraint is not modeled explicitly here. Instead, + ############################################################################ + # INITIALIZATION + ############################################################################ + + # The copy constraint is not modeled explicitly here. Instead, # the state variable is unfixed and takes the role of z in our paper. # It is then subtracted from the fixed value to obtain the so called slack. bw_data = node.ext[:backward_data] bw_data[:fixed_state_value] = Dict{Symbol,Float64}() - #bw_data[:bin_variables] = JuMP.VariableRef[] bw_data[:bin_constraints] = JuMP.ConstraintRef[] bw_data[:bin_states] = Dict{Symbol,JuMP.VariableRef}() bw_data[:bin_x_names] = Dict{Symbol,Symbol}() bw_data[:bin_k] = Dict{Symbol,Int64}() number_of_states = 0 + binary_precision = state_approximation_regime.binary_precision + ############################################################################ # PREPARE NEW STATE VARIABLES ############################################################################ for (state_name, value) in state @@ -48,17 +54,100 @@ function changeToBinarySpace!( epsilon = binary_precision[state_name] # Set up state for backward pass using binary approximation - setup_state_backward(subproblem, state_comp, state_name, epsilon, bw_data) + setup_state_binarization(subproblem, state_comp, state_name, epsilon, bw_data) end return end +""" +Trivial modification of the backward pass problem if no binary approximation +is used. +""" +function changeStateSpace!( + node::SDDP.Node, + subproblem::JuMP.Model, + state::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.NoStateApproximation + ) + + return +end + + +""" +Modifying the backward pass problem to regain the original states +if a binary approximation was used. +""" +function rechangeStateSpace!( + node::SDDP.Node, + subproblem::JuMP.Model, + state::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.BinaryApproximation + ) + + bw_data = node.ext[:backward_data] + + ############################################################################ + # FIX THE STATE VARIABLES AGAIN + ############################################################################ + for (state_name, value) in state + state_comp = node.states[state_name] + + JuMP.delete_lower_bound(state_comp.in) + JuMP.delete_upper_bound(state_comp.in) + + # unset binary or integer type + if JuMP.is_binary(state_comp.in) + JuMP.unset_binary(state_comp.in) + elseif JuMP.is_integer(state_comp.in) + JuMP.unset_integer(state_comp.in) + end + + JuMP.fix(state_comp.in, bw_data[:fixed_state_value][state_name]) + end + + ############################################################################ + # REPLACE THE NEW BY THE OLD OBJECTIVE + ############################################################################ + """ Note that this is already done in Kelley's method """ + + ############################################################################ + # DELETE ALL BINARY SPACE BASED VARIABLES AND CONSTRAINTS + ############################################################################ + for (name, bin_state) in bw_data[:bin_states] + JuMP.delete(subproblem, bin_state) + end + + for constraint in bw_data[:bin_constraints] + JuMP.delete(subproblem, constraint) + end + + delete!(node.ext, :backward_data) + +end + + +""" +Trivial modification of the backward pass problem to regain the original states +if no binary approximation was used. +""" +function rechangeStateSpace!( + node::SDDP.Node, + subproblem::JuMP.Model, + state::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.NoStateApproximation + ) + + return +end + + """ Setting up the binary state variables. """ -function setup_state_backward( +function setup_state_binarization( subproblem::JuMP.Model, state_comp::State, state_name::Symbol, @@ -69,17 +158,23 @@ function setup_state_backward( # Get name of state variable in String representation name = JuMP.name(state_comp.in) + ############################################################################ + # STATE IS ALREADY BINARY + ############################################################################ if state_comp.info.in.binary - #----------------------------------------------------------------------- - # In this case, the variable must not be unfixed, no new binary variables - # or constraints have to be introduced. + """ In this case, the variable must not be unfixed and, in principle, + no new variables or constraints have to be introduced. - # INTRODUCE ONE NEW BINARY VARIABLE TO THE PROBLEM - #################################################################### - # Still helpful, as later binary_vars is used in Lagrangian dual. - # If we would not introduce a new variable, but just store the state - # itself per reference in binary_vars, then it would be deleted later. + Still, we introduce a new binary variable (binary_var), as this one + is used when solving the Lagrangian dual. + + If we would not introduce a new variable, but just store the state + itself per reference in binary_vars, then it would be deleted later. + """ + ######################################################################## + # INTRODUCE ONE NEW BINARY VARIABLE TO THE PROBLEM + ######################################################################## binary_var = JuMP.@variable( subproblem, base_name = "bin_" * name, @@ -91,46 +186,47 @@ function setup_state_backward( bw_data[:bin_x_names][sym_name] = state_name bw_data[:bin_k][sym_name] = 1 + ######################################################################## # INTRODUCE BINARY EXPANSION CONSTRAINT TO THE PROBLEM - #################################################################### + ######################################################################## binary_constraint = JuMP.@constraint(subproblem, state_comp.in == binary_var) # store in list for later access and deletion push!(bw_data[:bin_constraints], binary_constraint) + ######################################################################## # FIX NEW VARIABLE - #################################################################### + ######################################################################## # Get fixed values from fixed value of original state fixed_binary_value = JuMP.fix_value(state_comp.in) # Fix binary variables #JuMP.unset_binary(binary_var.in) JuMP.fix(binary_var, fixed_binary_value) + ######################################################################## # UNFIX ORIGINAL STATE - #################################################################### + ######################################################################## # Unfix the original state JuMP.unfix(state_comp.in) follow_state_unfixing!(state_comp) - # DETERMINE BINARY APPROXIMATION STATE IN ORIGINAL COORDINATES - #################################################################### - #SDDP.bincontract([JuMP.fix_value(binary_vars[i]) for i = 1:num_vars]) - else if !isfinite(state_comp.info.in.upper_bound) || !state_comp.info.in.has_ub error("When using SDDiP, state variables require an upper bound.") end if state_comp.info.in.integer - #------------------------------------------------------------------- + #################################################################### + # STATE VARIABLE IS INTEGER + #################################################################### + """ + Note that we do not need to define the new "binary" variables as + binary, as they are fixed (to 0 or 1) or relaxed to [0,1] anyway. + Moreover, we do not need to define them as state variables. + """ + #################################################################### # INTRODUCE BINARY VARIABLES TO THE PROBLEM #################################################################### - #NOTE: We do not need to define them as binary, - #as they are either fixed or relaxed to [0, 1] anyway - #NOTE: We do not need to define them as state variables, - #as only the _in-parts would be required anyway. - #Moreover, no Binary/Integer or bound information has to be stored. - num_vars = SDDP._bitsrequired(state_comp.info.in.upper_bound) binary_vars = JuMP.@variable( @@ -142,13 +238,13 @@ function setup_state_backward( # store in list for later access and deletion for i in 1:num_vars - #push!(bw_data[:bin_variables], binary_vars[i]) sym_name = Symbol(JuMP.name(binary_vars[i])) bw_data[:bin_states][sym_name] = binary_vars[i] bw_data[:bin_x_names][sym_name] = state_name bw_data[:bin_k][sym_name] = i end + #################################################################### # INTRODUCE BINARY EXPANSION CONSTRAINT TO THE PROBLEM #################################################################### binary_constraint = JuMP.@constraint( @@ -158,6 +254,7 @@ function setup_state_backward( # store in list for later access and deletion push!(bw_data[:bin_constraints], binary_constraint) + #################################################################### # FIX NEW VARIABLES #################################################################### # Get fixed values from fixed value of original state @@ -168,28 +265,24 @@ function setup_state_backward( JuMP.fix(binary_vars[i], fixed_binary_values[i]) end + #################################################################### # UNFIX ORIGINAL STATE #################################################################### # Unfix the original state JuMP.unfix(state_comp.in) follow_state_unfixing!(state_comp) - # DETERMINE BINARY APPROXIMATION STATE IN ORIGINAL COORDINATES - #################################################################### - #SDDP.bincontract([JuMP.fix_value(binary_vars[i]) for i = 1:num_vars]) - else - #------------------------------------------------------------------- + #################################################################### + # STATE VARIABLE IS CONTINUOUS + #################################################################### epsilon = binary_precision + """ see comment above for integer case """ + + #################################################################### # INTRODUCE BINARY VARIABLES TO THE PROBLEM #################################################################### - #NOTE: We do not need to define them as binary, - #as they are either fixed or relaxed to [0, 1] anyway - #NOTE: We do not need to define them as state variables, - #as only the _in-parts would be required anyway. - #Moreover, no Binary/Integer or bound information has to be stored. - num_vars = SDDP._bitsrequired(round(Int, state_comp.info.in.upper_bound / epsilon)) binary_vars = JuMP.@variable( @@ -201,7 +294,6 @@ function setup_state_backward( subproblem[:binary_vars] = binary_vars # store in list for later access and deletion for i in 1:num_vars - #push!(bw_data[:bin_variables], binary_vars[i]) sym_name = Symbol(JuMP.name(binary_vars[i])) # Store binary state reference for later bw_data[:bin_states][sym_name] = binary_vars[i] @@ -210,6 +302,7 @@ function setup_state_backward( end subproblem[:binary_vars] = binary_vars + #################################################################### # INTRODUCE BINARY EXPANSION CONSTRAINT TO THE PROBLEM #################################################################### binary_constraint = JuMP.@constraint( @@ -220,6 +313,7 @@ function setup_state_backward( # store in list for later access and deletion push!(bw_data[:bin_constraints], binary_constraint) + #################################################################### # FIX NEW VARIABLES #################################################################### # Get fixed values from fixed value of original state @@ -230,68 +324,19 @@ function setup_state_backward( JuMP.fix(binary_vars[i], fixed_binary_values[i]) end + #################################################################### # UNFIX ORIGINAL STATE #################################################################### # Unfix the original state JuMP.unfix(state_comp.in) follow_state_unfixing!(state_comp) - - # DETERMINE BINARY APPROXIMATION STATE IN ORIGINAL COORDINATES - #################################################################### - #SDDP.bincontract([JuMP.fix_value(binary_vars[i]) for i = 1:num_vars], epsilon) end end + return end -""" -Modifying the backward pass problem to regain the original states. -""" -function changeToOriginalSpace!( - node::SDDP.Node, - subproblem::JuMP.Model, - state::Dict{Symbol,Float64} - ) - - bw_data = node.ext[:backward_data] - - # FIX THE STATE VARIABLES AGAIN - ############################################################################ - for (state_name, value) in state - state_comp = node.states[state_name] - - JuMP.delete_lower_bound(state_comp.in) - JuMP.delete_upper_bound(state_comp.in) - - # unset binary or integer type - if JuMP.is_binary(state_comp.in) - JuMP.unset_binary(state_comp.in) - elseif JuMP.is_integer(state_comp.in) - JuMP.unset_integer(state_comp.in) - end - - JuMP.fix(state_comp.in, bw_data[:fixed_state_value][state_name]) - end - - # REPLACE THE NEW BY THE OLD OBJECTIVE - ############################################################################ - # Maybe already done in Kelley's method - - # DELETE ALL BINARY SPACE BASED VARIABLES AND CONSTRAINTS - ############################################################################ - #delete(linearizedSubproblem, bw_data[:bin_variables]) - for (name, bin_state) in bw_data[:bin_states] - JuMP.delete(subproblem, bin_state) - end - - for constraint in bw_data[:bin_constraints] - JuMP.delete(subproblem, constraint) - end - - delete!(node.ext, :backward_data) - -end """ From 916e3d357678da72fe75c43fb65992f0a3cf2a9c Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Mon, 11 Oct 2021 23:20:01 -0400 Subject: [PATCH 09/30] Made some adjustments to newer SDDP.jl versions --- src/JuMP.jl | 10 +++++++--- src/objective.jl | 2 ++ src/state.jl | 33 +++++++++++++++++---------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/JuMP.jl b/src/JuMP.jl index ca38292f..cf553264 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -44,7 +44,7 @@ function JuMP.build_variable( ) end -function JuMP.add_variable(problem::JuMP.Model, state_info::StateInfo, name::String) +function JuMP.add_variable(subproblem::JuMP.Model, state_info::StateInfo, name::String) # Store bounds also in state, since they have to be relaxed and readded later # if state_info.out.has_lb @@ -65,8 +65,12 @@ function JuMP.add_variable(problem::JuMP.Model, state_info::StateInfo, name::Str state_info ) - integrality_handler = SDDP.get_integrality_handler(problem) - setup_state(problem, state, state_info, name, integrality_handler) + node = get_node(subproblem) + sym_name = Symbol(name) + @assert !haskey(node.states, sym_name) # JuMP prevents duplicate names. + node.states[sym_name] = state + graph = get_policy_graph(subproblem) + graph.initial_root_state[sym_name] = state_info.initial_value return state end diff --git a/src/objective.jl b/src/objective.jl index 6f16ae65..780b5463 100644 --- a/src/objective.jl +++ b/src/objective.jl @@ -30,6 +30,8 @@ function set_objective(subproblem::JuMP.Model) @expression( subproblem, node.stage_objective + + objective_state_component + + belief_state_component + bellman_term(node.bellman_function) ) ) diff --git a/src/state.jl b/src/state.jl index f1194fcc..52b07257 100644 --- a/src/state.jl +++ b/src/state.jl @@ -22,7 +22,7 @@ mutable struct StateInfo in::JuMP.VariableInfo out::JuMP.VariableInfo initial_value::Float64 - kwargs + kwargs::Any end struct State{T} @@ -34,21 +34,6 @@ struct State{T} info::StateInfo end -function setup_state( - subproblem::JuMP.Model, - state::State, - state_info::StateInfo, - name::String, - ::SDDP.ContinuousRelaxation -) - node = SDDP.get_node(subproblem) - sym_name = Symbol(name) - @assert !haskey(node.ext[:lin_states], sym_name) # JuMP prevents duplicate names. - node.ext[:lin_states][sym_name] = state - graph = SDDP.get_policy_graph(subproblem) - graph.ext[:lin_initial_root_state][sym_name] = state_info.initial_value - return -end # Internal function: set the incoming state variables of node to the values # contained in state. @@ -105,6 +90,8 @@ function prepare_state_fixing!(node::SDDP.Node, state_name::Symbol) elseif JuMP.is_integer(node.states[state_name].in) JuMP.unset_integer(node.states[state_name].in) end + + return end function prepare_state_fixing!(node::SDDP.Node, state::State) @@ -123,6 +110,8 @@ function prepare_state_fixing_binary!(node::SDDP.Node, state::JuMP.VariableRef) elseif JuMP.is_integer(state) JuMP.unset_integer(state) end + + return end # Reset binary and integer type of state variables. @@ -142,6 +131,7 @@ function follow_state_unfixing!(state::State) JuMP.set_integer(state.in) end + return end function follow_state_unfixing_binary!(state::JuMP.VariableRef) @@ -149,6 +139,7 @@ function follow_state_unfixing_binary!(state::JuMP.VariableRef) JuMP.set_lower_bound(state, 0) JuMP.set_upper_bound(state, 1) + return end struct BinaryState @@ -156,3 +147,13 @@ struct BinaryState x_name::Symbol # name of original state it is related to k::Int64 # index and exponent end + +function get_number_of_states(node::SDDP.Node, state_approximation_regime::DynamicSDDiP.BinaryApproximation) + + return length(node.ext[:backward_data][:bin_states]) +end + +function get_number_of_states(node::SDDP.Node, state_approximation_regime::DynamicSDDiP.NoStateApproximation) + + return length(node.states) +end From 03b16b7f78b1c7d1b6d6fae6ca49d6c749fb1c12 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Mon, 11 Oct 2021 23:25:29 -0400 Subject: [PATCH 10/30] Implemented new backward pass, which includes > Changes to Kelley's method according to new SDDP.jl version, e.g. Magnanti & Wong > Using new abstract types configuring the Lagrangian solution process > Differentiation between backward pass, duals and lagrange to increase code clarity > Some more changes and renamings in typedefs > Some further minor changes to adjust to the new SDDP.jl version > Level Bundle Method still has to be adapted, though --- src/algorithmMain.jl | 4 +- src/backwardPass.jl | 217 ++-------------- src/backwardPass0.jl | 562 +++++++++++++++++++++++++++++++++++++++ src/bellman.jl | 12 +- src/binarization.jl | 49 +++- src/duals.jl | 392 ++++++++++++++++++++++++++++ src/lagrange.jl | 577 ++++++++++++++++++++++------------------- src/logging.jl | 27 +- src/regularizations.jl | 39 ++- src/solverHandling.jl | 13 + src/typedefs.jl | 70 ++--- stuff.jl | 146 +++++++++++ 12 files changed, 1582 insertions(+), 526 deletions(-) create mode 100644 src/backwardPass0.jl create mode 100644 src/duals.jl diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index 6436c251..99f033c7 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -66,6 +66,7 @@ function solve( model.initial_root_state, time(), log, + log_file_handle ) ############################################################################ @@ -206,7 +207,7 @@ function master_loop(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, end # logging - log_iteration(algo_params, options.log) + log_iteration(algo_params, options.log_file_handle, options.log) # initialize parameters previous_solution = result.current_sol @@ -237,6 +238,7 @@ function master_loop(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, end + return end diff --git a/src/backwardPass.jl b/src/backwardPass.jl index d72224f0..dd885f37 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -1,11 +1,10 @@ # The functions # > "solve_all_children", # > "solve_subproblem_backward", -# > "get_dual_variables_backward", # > "calculate_bound", # > "solve_first_stage_problem" # are derived from similar named functions (backward_pass, -# solve_all_children, solve_subproblem, get_dual_variables, calculate_bound, +# solve_all_children, solve_subproblem, calculate_bound, # solve_first_stage_problem) in the 'SDDP.jl' package by # Oscar Dowson and released under the Mozilla Public License 2.0. # The reproduced function and other functions in this file are also released @@ -31,9 +30,9 @@ function backward_pass( objective_states::Vector{NTuple{N,Float64}}, belief_states::Vector{Tuple{Int,Dict{T,Float64}}}) where {T,NoiseType,N} - #################################################################### + ############################################################################ # INITIALIZATION - #################################################################### + ############################################################################ # storage for cuts cuts = Dict{T,Vector{Any}}(index => Any[] for index in keys(model.nodes)) @@ -45,7 +44,7 @@ function backward_pass( ############################################################################ # Traverse backwards through the stages ############################################################################ - for index = length(scenario_path):-1:1 + for index in length(scenario_path):-1:1 outgoing_state = sampled_states[index] items = BackwardPassItems(T, SDDP.Noise) @@ -62,9 +61,9 @@ function backward_pass( # but also to store them together with the symbol/name of the variable. node.ext[:binary_state_values] = Dict{Symbol, Float64}() - #################################################################### + ######################################################################## # SOLVE ALL CHILDREN PROBLEMS - #################################################################### + ######################################################################## solve_all_children( model, node, @@ -82,14 +81,7 @@ function backward_pass( # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS #################################################################### - anchor_points = Dict{Symbol,Float64}() - for (name, value) in outgoing_state - state_comp = node.states[name] - epsilon = algo_params.binary_precision[name] - (approx_state_value, ) = determine_anchor_states(state_comp, value, epsilon) - anchor_points[name] = approx_state_value - end - + anchor_states = determine_anchor_states(node, outgoing_state, algo_params.state_approximation_regime) @infiltrate algo_params.infiltrate_state in [:all] # REFINE BELLMAN FUNCTION BY ADDING CUTS @@ -102,7 +94,7 @@ function backward_pass( node.bellman_function, options.risk_measures[node_index], outgoing_state, - anchor_points, + anchor_states, items.bin_state, items.duals, items.supports, @@ -202,6 +194,8 @@ function solve_all_children( # Drop the last element (i.e., the one we added). pop!(scenario_path) end + + return end @@ -246,200 +240,35 @@ function solve_subproblem_backward( end ############################################################################ - # INITIALIZE DUALS - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "dual_initialization" begin - dual_vars_initial = initialize_duals(node, subproblem, algo_params.dual_initialization_method) - end - # RESET SOLVER (as it may have been changed in between for some reason) - ######################################################################## - DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :backward_pass) - - # GET PRIMAL SOLUTION TO BOUND LAGRANGIAN DUAL ############################################################################ - @infiltrate algo_params.infiltrate_state in [:all] - - # REGULARIZATION - if algo_params.regularization - node.ext[:regularization_data] = Dict{Symbol,Any}() - regularize_backward!(node, subproblem, algo_params.sigma[node_index]) - end - - # SOLVE PRIMAL PROBLEM (REGULARIZED OR NOT) - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_primal" begin - JuMP.optimize!(subproblem) - end - - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end + DynamicSDDiP.set_solver(subproblem, algo_params, applied_solvers, :backward_pass) - # NOTE: TO-DO: Attempt numerical recovery as in SDDP - - solver_obj = JuMP.objective_value(subproblem) - @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL - - @infiltrate algo_params.infiltrate_state in [:all] - - # PREPARE ACTUAL BACKWARD PASS METHOD BY DEREGULARIZATION ############################################################################ - if algo_params.regularization - deregularize_backward!(node, subproblem) - end - - # DUAL SOLUTION + # SOLVE DUAL PROBLEM TO OBTAIN CUT INFORMATION ############################################################################ - # Solve dual and return a dict with the multiplier of the copy constraints. - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_lagrange" begin - lagrangian_results = get_dual_variables_backward(node, node_index, solver_obj, algo_params, applied_solvers, dual_vars_initial) + # Solve dual and return a dict with the multipliers of the copy constraints. + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_dual" begin + dual_results = get_dual_solution(node, node_index, solver_obj, algo_params, applied_solvers, algo_params.duality_regime) end - dual_values = lagrangian_results.dual_values - bin_state = lagrangian_results.bin_state - objective = lagrangian_results.intercept - iterations = lagrangian_results.iterations - lag_status = lagrangian_results.lag_status - - @infiltrate algo_params.infiltrate_state in [:all] - ############################################################################ - # REGAIN ORIGINAL MODEL + # REGAIN ORIGINAL MODEL IF BINARY APPROXIMATION IS USED ############################################################################ TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin rechangeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) end - return ( - duals = dual_values, - bin_state = bin_state, - objective = objective, - iterations = iterations, - lag_status = lag_status, - ) -end - - -""" -Calling the Lagrangian dual solution method and determining dual variables -required to construct a cut -""" -function get_dual_variables_backward( - node::SDDP.Node, - node_index::Int64, - solver_obj::Float64, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers, - dual_vars_initial::Vector{Float64} - ) - - # storages for return of dual values and binary state values (trial point) - dual_values = Dict{Symbol,Float64}() - bin_state = Dict{Symbol, BinaryState}() - - # TODO: implement smart choice for initial duals - number_of_states = length(node.ext[:backward_data][:bin_states]) - # dual_vars = zeros(number_of_states) - #solver_obj = JuMP.objective_value(node.ext[:linSubproblem]) - dual_vars = dual_vars_initial - - lag_obj = 0 - lag_iterations = 0 - lag_status = :none - - # Create an SDDiP integrality_handler here to store the Lagrangian dual information - #TODO: Store tolerances in algo_params - integrality_handler = SDDP.SDDiP(iteration_limit = algo_params.lagrangian_iteration_limit, atol = algo_params.lagrangian_atol, rtol = algo_params.lagrangian_rtol) - integrality_handler = SDDP.update_integrality_handler!(integrality_handler, applied_solvers.MILP, number_of_states) - node.ext[:lagrange] = integrality_handler - - # DETERMINE AND ADD BOUNDS FOR DUAL VARIABLES - ############################################################################ - #TODO: Determine a norm of B (coefficient matrix of binary expansion) - # We use the column sum norm here - # But instead of calculating it exactly, we can also use the maximum - # upper bound of all state variables as a bound - - B_norm_bound = 0 - for (name, state_comp) in node.states - if state_comp.info.in.upper_bound > B_norm_bound - B_norm_bound = state_comp.info.in.upper_bound - end - end - dual_bound = algo_params.sigma[node_index] * B_norm_bound - - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - #|| node.ext[:linSubproblem].ext[:sddp_policy_graph].ext[:iteration] == 12 - - try - # SOLUTION WITHOUT BOUNDED DUAL VARIABLES (BETTER TO OBTAIN BASIC SOLUTIONS) - ######################################################################## - if algo_params.lagrangian_method == :kelley - results = _kelley(node, node_index, solver_obj, dual_vars, integrality_handler, algo_params, applied_solvers, nothing) - lag_obj = results.lag_obj - lag_iterations = results.iterations - lag_status = results.lag_status - elseif algo_params.lagrangian_method == :bundle_level - results = _bundle_level(node, node_index, solver_obj, dual_vars, integrality_handler, algo_params, applied_solvers, nothing) - lag_obj = results.lag_obj - lag_iterations = results.iterations - lag_status = results.lag_status - end - - # OPTIMAL VALUE CHECKS - ######################################################################## - if algo_params.lag_status_regime == :rigorous - if lag_status == :conv - error("Lagrangian dual converged to value < solver_obj.") - elseif lag_status == :sub - error("Lagrangian dual had subgradients zero without LB=UB.") - elseif lag_status == :iter - error("Solving Lagrangian dual exceeded iteration limit.") - end - - elseif algo_params.lag_status_regime == :lax - # all cuts will be used as they are valid even though not necessarily tight - end - - # DUAL VARIABLE BOUND CHECK - ######################################################################## - # if one of the dual variables exceeds the bounds (e.g. in case of an - # discontinuous value function), use bounded version of Kelley's method - bound_check = true - for dual_var in dual_vars - if dual_var > dual_bound - bound_check = false - end - end - - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - - catch e - SDDP.write_subproblem_to_file(node, "subproblem.mof.json", throw_error = false) - rethrow(e) - end - - # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN - ############################################################################ - for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) - # TODO (maybe) change dual signs inside kelley to match LP duals - dual_values[name] = -dual_vars[i] - - value = integrality_handler.old_rhs[i] - x_name = node.ext[:backward_data][:bin_x_names][name] - k = node.ext[:backward_data][:bin_k][name] - bin_state[name] = BinaryState(value, x_name, k) - end + @infiltrate algo_params.infiltrate_state in [:all] return ( - dual_values=dual_values, - bin_state=bin_state, - intercept=lag_obj, - iterations=lag_iterations, - lag_status=lag_status, + duals = dual_results.dual_values, + bin_state = dual_results.bin_state, + objective = dual_results.intercept, + iterations = dual_results.iterations, + lag_status = dual_results.lag_status, ) + end diff --git a/src/backwardPass0.jl b/src/backwardPass0.jl new file mode 100644 index 00000000..d44a83aa --- /dev/null +++ b/src/backwardPass0.jl @@ -0,0 +1,562 @@ +# The functions +# > "solve_all_children", +# > "solve_subproblem_backward", +# > "get_dual_variables_backward", +# > "calculate_bound", +# > "solve_first_stage_problem" +# are derived from similar named functions (backward_pass, +# solve_all_children, solve_subproblem, get_dual_variables, calculate_bound, +# solve_first_stage_problem) in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Executing the backward pass of a loop of DynamicSDDiP. +""" +function backward_pass( + model::SDDP.PolicyGraph{T}, + options::DynamicSDDiP.Options, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + scenario_path::Vector{Tuple{T,NoiseType}}, + sampled_states::Vector{Dict{Symbol,Float64}}, + objective_states::Vector{NTuple{N,Float64}}, + belief_states::Vector{Tuple{Int,Dict{T,Float64}}}) where {T,NoiseType,N} + + #################################################################### + # INITIALIZATION + #################################################################### + + # storage for cuts + cuts = Dict{T,Vector{Any}}(index => Any[] for index in keys(model.nodes)) + + # storage for data on solving Lagrangian dual + model.ext[:lag_iterations] = Int[] + model.ext[:lag_status] = Symbol[] + + ############################################################################ + # Traverse backwards through the stages + ############################################################################ + for index in length(scenario_path):-1:1 + outgoing_state = sampled_states[index] + items = BackwardPassItems(T, SDDP.Noise) + + node_index, _ = scenario_path[index] + node = model[node_index] + if length(node.children) == 0 + continue + end + + # Dict to store values of binary approximation of the state + # Note that we could also retrieve this from the actual trial point + # (outgoing_state) or from its approximation via binexpand. However, + # this collection is not only important to obtain the correct values, + # but also to store them together with the symbol/name of the variable. + node.ext[:binary_state_values] = Dict{Symbol, Float64}() + + #################################################################### + # SOLVE ALL CHILDREN PROBLEMS + #################################################################### + solve_all_children( + model, + node, + node_index, + items, + 1.0, + belief_state, + objective_state, + outgoing_state, + algo_params.backward_sampling_scheme, + scenario_path[1:index], + algo_params, + applied_solvers + ) + + # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS + #################################################################### + anchor_points = Dict{Symbol,Float64}() + for (name, value) in outgoing_state + state_comp = node.states[name] + epsilon = algo_params.binary_precision[name] + (approx_state_value, ) = determine_anchor_states(state_comp, value, epsilon) + anchor_points[name] = approx_state_value + end + + @infiltrate algo_params.infiltrate_state in [:all] + + # REFINE BELLMAN FUNCTION BY ADDING CUTS + #################################################################### + TimerOutputs.@timeit DynamicSDDiP_TIMER "update_bellman" begin + new_cuts = refine_bellman_function( + model, + node, + node_index, + node.bellman_function, + options.risk_measures[node_index], + outgoing_state, + anchor_points, + items.bin_state, + items.duals, + items.supports, + items.probability, + items.objectives, + algo_params, + applied_solvers + ) + end + push!(cuts[node_index], new_cuts) + #NOTE: Has to be adapted for stochastic case + push!(model.ext[:lag_iterations], sum(items.lag_iterations)) + push!(model.ext[:lag_status], items.lag_status[1]) + + #TODO: Implement cut-sharing as in SDDP + + end + return cuts +end + + +""" +Solving all children within the backward pass. +""" +function solve_all_children( + model::SDDP.PolicyGraph{T}, + node::SDDP.Node{T}, + node_index::Int64, + items::BackwardPassItems, + belief::Float64, + belief_state, + objective_state, + outgoing_state::Dict{Symbol,Float64}, + backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme, + scenario_path, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers +) where {T} + length_scenario_path = length(scenario_path) + for child in node.children + if isapprox(child.probability, 0.0, atol = 1e-6) + continue + end + child_node = model[child.term] + for noise in SDDP.sample_backward_noise_terms(backward_sampling_scheme, child_node) + if length(scenario_path) == length_scenario_path + push!(scenario_path, (child.term, noise.term)) + else + scenario_path[end] = (child.term, noise.term) + end + #################################################################### + # IF SOLUTIONS FOR THIS NODE ARE CACHED ALREADY, USE THEM + #################################################################### + if haskey(items.cached_solutions, (child.term, noise.term)) + sol_index = items.cached_solutions[(child.term, noise.term)] + push!(items.duals, items.duals[sol_index]) + push!(items.supports, items.supports[sol_index]) + push!(items.nodes, child_node.index) + push!(items.probability, items.probability[sol_index]) + push!(items.objectives, items.objectives[sol_index]) + push!(items.belief, belief) + push!(items.bin_state, items.bin_state[sol_index]) + push!(items.lag_iterations, items.lag_iterations[sol_index]) + push!(items.lag_status, items.lag_status[sol_index]) + else + ################################################################ + # SOLVE THE BACKWARD PASS PROBLEM + ################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_BP" begin + subproblem_results = solve_subproblem_backward( + model, + child_node, + node_index+1, + outgoing_state, + noise.term, + scenario_path, + algo_params, + applied_solvers + ) + end + push!(items.duals, subproblem_results.duals) + push!(items.supports, noise) + push!(items.nodes, child_node.index) + push!(items.probability, child.probability * noise.probability) + push!(items.objectives, subproblem_results.objective) + push!(items.belief, belief) + push!(items.bin_state, subproblem_results.bin_state) + push!(items.lag_iterations, subproblem_results.iterations) + push!(items.lag_status, subproblem_results.lag_status) + items.cached_solutions[(child.term, noise.term)] = length(items.duals) + end + end + end + if length(scenario_path) == length_scenario_path + # No-op. There weren't any children to solve. + else + # Drop the last element (i.e., the one we added). + pop!(scenario_path) + end +end + + +""" +Solving the backward pass problem for one specific child +""" +# Internal function: solve the subproblem associated with node given the +# incoming state variables state and realization of the stagewise-independent +# noise term noise. If require_duals=true, also return the dual variables +# associated with the fixed constraint of the incoming state variables. +function solve_subproblem_backward( + model::SDDP.PolicyGraph{T}, + node::SDDP.Node{T}, + node_index::Int64, + state::Dict{Symbol,Float64}, + noise, + scenario_path::Vector{Tuple{T,S}}, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers; +) where {T,S} + + ############################################################################ + # MODEL PARAMETRIZATION + ############################################################################ + subproblem = node.subproblem + + # Storage for backward pass data + node.ext[:backward_data] = Dict{Symbol,Any}() + + # Parameterize the model. Fix the value of the incoming state variables. + # Then parameterize the model depending on `noise` and set the objective. + set_incoming_state(node, state) + parameterize(node, noise) + + @infiltrate algo_params.infiltrate_state in [:all] + + ############################################################################ + # CHANGE STATE SPACE IF BINARY APPROXIMATION IS USED + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin + changeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) + end + + ############################################################################ + # INITIALIZE DUALS + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "dual_initialization" begin + dual_vars_initial = initialize_duals(node, subproblem, algo_params.dual_initialization_method) + end + + # RESET SOLVER (as it may have been changed in between for some reason) + ######################################################################## + DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :backward_pass) + + # GET PRIMAL SOLUTION TO BOUND LAGRANGIAN DUAL + ############################################################################ + @infiltrate algo_params.infiltrate_state in [:all] + + # REGULARIZATION + if algo_params.regularization + node.ext[:regularization_data] = Dict{Symbol,Any}() + regularize_backward!(node, subproblem, algo_params.sigma[node_index]) + end + + # SOLVE PRIMAL PROBLEM (REGULARIZED OR NOT) + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_primal" begin + JuMP.optimize!(subproblem) + end + + if haskey(model.ext, :total_solves) + model.ext[:total_solves] += 1 + else + model.ext[:total_solves] = 1 + end + + # NOTE: TO-DO: Attempt numerical recovery as in SDDP + + solver_obj = JuMP.objective_value(subproblem) + @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL + + @infiltrate algo_params.infiltrate_state in [:all] + + # PREPARE ACTUAL BACKWARD PASS METHOD BY DEREGULARIZATION + ############################################################################ + if algo_params.regularization + deregularize_backward!(node, subproblem) + end + + # DUAL SOLUTION + ############################################################################ + # Solve dual and return a dict with the multiplier of the copy constraints. + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_lagrange" begin + lagrangian_results = get_dual_variables_backward(node, node_index, solver_obj, algo_params, applied_solvers, dual_vars_initial) + end + + dual_values = lagrangian_results.dual_values + bin_state = lagrangian_results.bin_state + objective = lagrangian_results.intercept + iterations = lagrangian_results.iterations + lag_status = lagrangian_results.lag_status + + @infiltrate algo_params.infiltrate_state in [:all] + + ############################################################################ + # REGAIN ORIGINAL MODEL + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin + rechangeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) + end + + return ( + duals = dual_values, + bin_state = bin_state, + objective = objective, + iterations = iterations, + lag_status = lag_status, + ) +end + + +""" +Calling the Lagrangian dual solution method and determining dual variables +required to construct a cut +""" +function get_dual_variables_backward( + node::SDDP.Node, + node_index::Int64, + solver_obj::Float64, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + dual_vars_initial::Vector{Float64} + ) + + # storages for return of dual values and binary state values (trial point) + dual_values = Dict{Symbol,Float64}() + bin_state = Dict{Symbol, BinaryState}() + + # TODO: implement smart choice for initial duals + number_of_states = length(node.ext[:backward_data][:bin_states]) + # dual_vars = zeros(number_of_states) + #solver_obj = JuMP.objective_value(node.ext[:linSubproblem]) + dual_vars = dual_vars_initial + + lag_obj = 0 + lag_iterations = 0 + lag_status = :none + + # Create an SDDiP integrality_handler here to store the Lagrangian dual information + #TODO: Store tolerances in algo_params + integrality_handler = SDDP.SDDiP(iteration_limit = algo_params.lagrangian_iteration_limit, atol = algo_params.lagrangian_atol, rtol = algo_params.lagrangian_rtol) + integrality_handler = SDDP.update_integrality_handler!(integrality_handler, applied_solvers.MILP, number_of_states) + node.ext[:lagrange] = integrality_handler + + # DETERMINE AND ADD BOUNDS FOR DUAL VARIABLES + ############################################################################ + #TODO: Determine a norm of B (coefficient matrix of binary expansion) + # We use the column sum norm here + # But instead of calculating it exactly, we can also use the maximum + # upper bound of all state variables as a bound + + B_norm_bound = 0 + for (name, state_comp) in node.states + if state_comp.info.in.upper_bound > B_norm_bound + B_norm_bound = state_comp.info.in.upper_bound + end + end + dual_bound = algo_params.sigma[node_index] * B_norm_bound + + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] + #|| node.ext[:linSubproblem].ext[:sddp_policy_graph].ext[:iteration] == 12 + + try + # SOLUTION WITHOUT BOUNDED DUAL VARIABLES (BETTER TO OBTAIN BASIC SOLUTIONS) + ######################################################################## + if algo_params.lagrangian_method == :kelley + results = _kelley(node, node_index, solver_obj, dual_vars, integrality_handler, algo_params, applied_solvers, nothing) + lag_obj = results.lag_obj + lag_iterations = results.iterations + lag_status = results.lag_status + elseif algo_params.lagrangian_method == :bundle_level + results = _bundle_level(node, node_index, solver_obj, dual_vars, integrality_handler, algo_params, applied_solvers, nothing) + lag_obj = results.lag_obj + lag_iterations = results.iterations + lag_status = results.lag_status + end + + # OPTIMAL VALUE CHECKS + ######################################################################## + if algo_params.lag_status_regime == :rigorous + if lag_status == :conv + error("Lagrangian dual converged to value < solver_obj.") + elseif lag_status == :sub + error("Lagrangian dual had subgradients zero without LB=UB.") + elseif lag_status == :iter + error("Solving Lagrangian dual exceeded iteration limit.") + end + + elseif algo_params.lag_status_regime == :lax + # all cuts will be used as they are valid even though not necessarily tight + end + + # DUAL VARIABLE BOUND CHECK + ######################################################################## + # if one of the dual variables exceeds the bounds (e.g. in case of an + # discontinuous value function), use bounded version of Kelley's method + bound_check = true + for dual_var in dual_vars + if dual_var > dual_bound + bound_check = false + end + end + + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] + + catch e + SDDP.write_subproblem_to_file(node, "subproblem.mof.json", throw_error = false) + rethrow(e) + end + + # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN + ############################################################################ + for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) + # TODO (maybe) change dual signs inside kelley to match LP duals + dual_values[name] = -dual_vars[i] + + value = integrality_handler.old_rhs[i] + x_name = node.ext[:backward_data][:bin_x_names][name] + k = node.ext[:backward_data][:bin_k][name] + bin_state[name] = BinaryState(value, x_name, k) + end + + return ( + dual_values=dual_values, + bin_state=bin_state, + intercept=lag_obj, + iterations=lag_iterations, + lag_status=lag_status, + ) +end + + +""" +Calculate the lower bound (if minimizing, otherwise upper bound) of the problem +model at the point state. +""" +function calculate_bound( + model::SDDP.PolicyGraph{T}, + root_state::Dict{Symbol,Float64} = model.initial_root_state; + risk_measure = SDDP.Expectation(), +) where {T} + + # Note that here all children of the root node are solved, since the root + # node is not node 1, but node 0. + # In our case, this means that only stage 1 problem is solved again, + # using the updated Bellman function from the backward pass. + + # Initialization. + noise_supports = Any[] + probabilities = Float64[] + objectives = Float64[] + problem_size = Dict{Symbol,Int64}[] + current_belief = SDDP.initialize_belief(model) + + # Solve all problems that are children of the root node. + for child in model.root_children + if isapprox(child.probability, 0.0, atol = 1e-6) + continue + end + node = model[child.term] + for noise in node.noise_terms + subproblem_results = solve_first_stage_problem( + model, + node, + root_state, + noise.term, + Tuple{T,Any}[(child.term, noise.term)], + ) + push!(objectives, subproblem_results.objective) + push!(probabilities, child.probability * noise.probability) + push!(noise_supports, noise.term) + push!(problem_size, subproblem_results.problem_size) + end + end + # Now compute the risk-adjusted probability measure: + risk_adjusted_probability = similar(probabilities) + offset = SDDP.adjust_probability( + risk_measure, + risk_adjusted_probability, + probabilities, + noise_supports, + objectives, + model.objective_sense == MOI.MIN_SENSE, + ) + # Finally, calculate the risk-adjusted value. + return (bound = sum(obj * prob for (obj, prob) in zip(objectives, risk_adjusted_probability)) + + offset, problem_size = problem_size) +end + + +""" +Solving the first-stage problem to determine a lower bound +""" +function solve_first_stage_problem( + model::SDDP.PolicyGraph{T}, + node::SDDP.Node{T}, + state::Dict{Symbol,Float64}, + noise, + scenario_path::Vector{Tuple{T,S}}; +) where {T,S} + + ############################################################################ + # MODEL PARAMETRIZATION + ############################################################################ + subproblem = node.subproblem + + # Parameterize the model. First, fix the value of the incoming state + # variables. Then parameterize the model depending on `noise`. Finally, + # set the objective. + set_incoming_state(node, state) + parameterize(node, noise) + + ############################################################################ + # SOLUTION + ############################################################################ + JuMP.optimize!(subproblem) + + if haskey(model.ext, :total_solves) + model.ext[:total_solves] += 1 + else + model.ext[:total_solves] = 1 + end + + # Maybe attempt numerical recovery as in SDDP + + state = get_outgoing_state(node) + stage_objective = JuMP.value(node.stage_objective) + objective = JuMP.objective_value(subproblem) + dual_values = get_dual_variables(node, node.integrality_handler) + + ############################################################################ + # DETERMINE THE PROBLEM SIZE + ############################################################################ + problem_size = Dict{Symbol,Int64}() + problem_size[:total_var] = size(JuMP.all_variables(subproblem),1) + problem_size[:bin_var] = JuMP.num_constraints(subproblem, VariableRef, MOI.ZeroOne) + problem_size[:int_var] = JuMP.num_constraints(subproblem, VariableRef, MOI.Integer) + problem_size[:total_con] = JuMP.num_constraints(subproblem, GenericAffExpr{Float64,VariableRef}, MOI.LessThan{Float64}) + + JuMP.num_constraints(subproblem, GenericAffExpr{Float64,VariableRef}, MOI.GreaterThan{Float64}) + + JuMP.num_constraints(subproblem, GenericAffExpr{Float64,VariableRef}, MOI.EqualTo{Float64}) + + return ( + state = state, + duals = dual_values, + objective = objective, + stage_objective = stage_objective, + problem_size = problem_size + ) +end diff --git a/src/bellman.jl b/src/bellman.jl index db5a023a..ba36c06e 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -241,7 +241,7 @@ function _add_average_cut( πᵏ = Dict(key => 0.0 for key in keys(bin_states)) θᵏ = offset - for i = 1:length(objective_realizations) + for i in 1:length(objective_realizations) p = risk_adjusted_probability[i] θᵏ += p * objective_realizations[i] for (key, dual) in dual_variables[i] @@ -486,7 +486,7 @@ function add_cut_constraints_to_models( ############################################################################ expr = @expression( model, - V.theta + yᵀμ - sum(allCoefficients[j] * gamma[j] for j=1:number_of_duals) + V.theta + yᵀμ - sum(allCoefficients[j] * gamma[j] for j in 1:number_of_duals) ) constraint_ref = if JuMP.objective_sense(model) == MOI.MIN_SENSE @@ -500,7 +500,7 @@ function add_cut_constraints_to_models( ############################################################################ expr_lin = @expression( model_lin, - V_lin.theta + yᵀμ - sum(allCoefficients[j] * gamma_lin[j] for j=1:number_of_duals) + V_lin.theta + yᵀμ - sum(allCoefficients[j] * gamma_lin[j] for j in 1:number_of_duals) ) constraint_ref_lin = if JuMP.objective_sense(model_lin) == MOI.MIN_SENSE @@ -561,7 +561,7 @@ function add_cut_projection_to_model!( #################################################################### binary_constraint = JuMP.@constraint( model, - state_comp == SDDP.bincontract([γ[k] for k = 1:K], epsilon) + state_comp == SDDP.bincontract([γ[k] for k in 1:K], epsilon) ) push!(cutConstraints, binary_constraint) @@ -858,7 +858,7 @@ function _eval_height(node::SDDP.Node, cut::NCNBD.NonlinearCut, states::Dict{Sym binary_variables_so_far += K # introduce binary expansion constraint to the model - binary_constraint = JuMP.@constraint(model, SDDP.bincontract([binary_var[k] for k=1:K], epsilon) == value) + binary_constraint = JuMP.@constraint(model, SDDP.bincontract([binary_var[k] for k in 1:K], epsilon) == value) # determine the correct cut coefficient relatedCoefficients = Vector{Float64}(undef, K) @@ -887,7 +887,7 @@ function _eval_height(node::SDDP.Node, cut::NCNBD.NonlinearCut, states::Dict{Sym JuMP.@objective( model, eval_sense, - cut.intercept + sum(allCoefficients[j] * binary_state_storage[j] for j=1:binary_variables_so_far) + cut.intercept + sum(allCoefficients[j] * binary_state_storage[j] for j in 1:binary_variables_so_far) ) # SOLVE MODEL AND RETURN SOLUTION diff --git a/src/binarization.jl b/src/binarization.jl index ab6b7182..c754dd7e 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -126,6 +126,8 @@ function rechangeStateSpace!( delete!(node.ext, :backward_data) + return + end @@ -249,7 +251,7 @@ function setup_state_binarization( #################################################################### binary_constraint = JuMP.@constraint( subproblem, - state_comp.in == SDDP.bincontract([binary_vars[i] for i = 1:num_vars]) + state_comp.in == SDDP.bincontract([binary_vars[i] for i in 1:num_vars]) ) # store in list for later access and deletion push!(bw_data[:bin_constraints], binary_constraint) @@ -260,7 +262,7 @@ function setup_state_binarization( # Get fixed values from fixed value of original state fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], state_comp.info.in.upper_bound) # Fix binary variables - for i = 1:num_vars + for i in 1:num_vars #JuMP.unset_binary(binary_vars[i].in) JuMP.fix(binary_vars[i], fixed_binary_values[i]) end @@ -307,7 +309,7 @@ function setup_state_binarization( #################################################################### binary_constraint = JuMP.@constraint( subproblem, - state_comp.in == SDDP.bincontract([binary_vars[i] for i = 1:num_vars], epsilon) + state_comp.in == SDDP.bincontract([binary_vars[i] for i in 1:num_vars], epsilon) ) # store in list for later access and deletion @@ -319,7 +321,7 @@ function setup_state_binarization( # Get fixed values from fixed value of original state fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], state_comp.info.in.upper_bound, epsilon) # Fix binary variables - for i = 1:num_vars + for i in 1:num_vars #JuMP.unset_binary(binary_vars[i].in) JuMP.fix(binary_vars[i], fixed_binary_values[i]) end @@ -337,12 +339,32 @@ function setup_state_binarization( end +""" +Determining the anchor points in the original space if BinaryApproximation +is used. +""" +function determine_anchor_states( + node::DynamicSDDiP.Node, + outgoing_state::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.BinaryApproximation, +) + + anchor_states = Dict{Symbol,Float64}() + for (name, value) in outgoing_state + state_comp = node.states[name] + epsilon = state_approximation_regime.binary_precision[name] + (approx_state_value, ) = determine_anchor_state(state_comp, value, epsilon) + anchor_states[name] = approx_state_value + end + + return anchor_states +end """ -Determining the anchor points in the original space. +Determining a single anchor state in the original space. """ -function determine_anchor_states( +function determine_anchor_state( state_comp::State, state_value::Float64, binaryPrecision::Float64, @@ -366,3 +388,18 @@ function determine_anchor_states( end return approx_state_value, fixed_binary_values end + + +""" +Determining the anchor points in the original space if no state approximation +is used. +""" +function determine_anchor_states( + node::DynamicSDDiP.Node, + outgoing_state::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.NoStateApproximation, +) + + anchor_states = Dict{Symbol,Float64}() + return anchor_states +end diff --git a/src/duals.jl b/src/duals.jl new file mode 100644 index 00000000..dc533726 --- /dev/null +++ b/src/duals.jl @@ -0,0 +1,392 @@ +# The function +# > "get_dual_solution", +# is derived from the same function in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + + +""" +Solving the dual problem to obtain cut information - using LP relaxation +""" +function get_dual_solution( + node::SDDP.Node, + node_index::Int64, + solver_obj::Float64, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + duality_regime::DynamicSDDiP.LinearDuality, + ) + +end + +""" +Solving the dual problem to obtain cut information - using LP relaxation +and strengthening by Lagrangian relaxation +""" +function get_dual_solution( + node::SDDP.Node, + node_index::Int64, + solver_obj::Float64, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + duality_regime::DynamicSDDiP.StrengthenedDuality, + ) + +end + +""" +Solving the dual problem to obtain cut information - using Lagrangian dual +""" +function get_dual_solution( + node::SDDP.Node, + node_index::Int64, + solver_obj::Float64, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + duality_regime::DynamicSDDiP.LagrangianDuality, + ) + + ############################################################################ + # SOME INITIALIZATIONS + ############################################################################ + subproblem = node.subproblem + + # storages for return of dual values and binary state values (trial point) + # note that with NoStateApproximation bin_state will just remain empty + dual_values = Dict{Symbol,Float64}() + bin_state = Dict{Symbol, BinaryState}() + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + + # storages for information on Lagrangian dual + lag_obj = 0 + lag_iterations = 0 + lag_status = :none + + ############################################################################ + # INITIALIZE DUALS + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "dual_initialization" begin + dual_vars = initialize_duals(node, subproblem, algo_params, applied_solvers, duality_regime.dual_initialization_regime) + end + + ############################################################################ + # GET PRIMAL SOLUTION TO BOUND LAGRANGIAN DUAL, TRACK CONVERGENCE AND DEBUGGING + ############################################################################ + # REGULARIZE PROBLEM IF REGULARIZATION IS USED + node.ext[:regularization_data] = Dict{Symbol,Any}() + regularize_binary!(node, node_index, subproblem, algo_params.regularization_regime) + + # SOLVE PRIMAL PROBLEM (can be regularized or not) + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_primal" begin + JuMP.optimize!(subproblem) + end + + # Maybe attempt numerical recovery as in SDDP + primal_obj = JuMP.objective_value(subproblem) + @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL + + # ADAPT TOTAL SOLVES APPROPRIATELY + if haskey(model.ext, :total_solves) + model.ext[:total_solves] += 1 + else + model.ext[:total_solves] = 1 + end + + # DEREGULARIZE PROBLEM IF REQUIRED + deregularize_binary!(node, subproblem, algo_params.regularization_regime) + + @infiltrate algo_params.infiltrate_state in [:all] + + ############################################################################ + # GET BOUNDS FOR LAGRANGIAN DUAL + ############################################################################ + bound_results = get_dual_bounds(node, algo_params, primal_obj, duality_regime.dual_bound_regime) + + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] + + try + ######################################################################## + # CALL SOLUTION METHOD + ######################################################################## + # Solve dual and return a dict with the multiplier of the copy constraints. + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_lagrange" begin + results = solve_lagrangian_dual( + node, + node_index, + primal_obj, + dual_vars, + bound_results, + integrality_handler, + algo_params, + applied_solvers, + duality_regime.dual_solution_regime + ) + end + + lag_obj = results.lag_obj + lag_iterations = results.iterations + lag_status = results.lag_status + + ######################################################################## + # CHECK STATUS FOR ABNORMAL BEHAVIOR + ######################################################################## + # if status is not as intended, the algorithm terminates with an error + lagrangian_status_check(lag_status, duality_regime.dual_status_regime) + + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] + + catch e + SDDP.write_subproblem_to_file(node, "subproblem.mof.json", throw_error = false) + rethrow(e) + end + + ############################################################################ + # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN + ############################################################################ + store_dual_values!(node, dual_vars, binary_state, integrality_handler, algo_params.state_approximation_regime) + + return ( + dual_values=dual_values, + bin_state=bin_state, + intercept=lag_obj, + iterations=lag_iterations, + lag_status=lag_status, + ) + + +""" +Determining objective and/or variable bounds for the Lagrangian dual +if ValueBound is used. + +Note that we always solve the primal problem, even if we do not use its +objective value as the objective bound, as this is more convenient for +debugging purposes. +""" +function get_dual_bounds( + node::SDDP.Node, + node_index::Int64, + algo_params::DynamicSDDiP.AlgoParams, + primal_obj::Float64, + dual_bound_regime::DynamicSDDiP.ValueBound, + ) + + return ( + obj_bound = primal_obj, + dual_bound = Inf + ) + +end + +""" +Determining objective and/or variable bounds for the Lagrangian dual +if NormBound is used. + +Note that we always solve the primal problem, even if we do not use its +objective value as the objective bound, as this is more convenient for +debugging purposes. +""" +function get_dual_bounds( + node::SDDP.Node, + node_index::Int64, + algo_params::DynamicSDDiP.AlgoParams, + primal_obj::Float64, + dual_bound_regime::DynamicSDDiP.NormBound, + ) + + return ( + obj_bound = Inf, + dual_bound = get_norm_bound(node, node_index, algo_params) + ) + +end + +""" +Determining objective and/or variable bounds for the Lagrangian dual +if BothBound is used. + +Note that we always solve the primal problem, even if we do not use its +objective value as the objective bound, as this is more convenient for +debugging purposes. +""" +function get_dual_bounds( + node::SDDP.Node, + node_index::Int64, + algo_params::DynamicSDDiP.AlgoParams, + primal_obj::Float64, + dual_bound_regime::DynamicSDDiP.BothBounds, + ) + + return ( + obj_bound = primal_obj, + dual_bound = get_norm_bound(node, node_index, algo_params) + ) + +end + +""" +Determining the norm bound to be used for the Lagrangian dual. + +Actually, we attempt to calculate a norm of B (coefficient matrix of the +binary expansion), e.g. the column sum norm. However, we can also use an +overestimator by taking the maximum upper bound of all state variables. +""" +function get_norm_bound( + node::SDDP.Node, + node_index::Int64, + algo_params::DynamicSDDiP.AlgoParams, + ) + + B_norm_bound = 0 + for (name, state_comp) in node.states + if state_comp.info.in.upper_bound > B_norm_bound + B_norm_bound = state_comp.info.in.upper_bound + end + end + dual_bound = algo_params.regularization_regime.sigma[node_index] * B_norm_bound + + return dual_bound +end + + +""" +Checking the status of the Lagrangian dual solution and throw an error if required +under rigorous regime. +""" +function lagrangian_status_check( + lag_status::Symbol, + dual_status_regime::DynamicSDDiP.Rigorous, + ) + + if lag_status == :conv + error("Lagrangian dual converged to value < solver_obj.") + elseif lag_status == :sub + error("Lagrangian dual had subgradients zero without LB=UB.") + elseif lag_status == :iter + error("Solving Lagrangian dual exceeded iteration limit.") + end + + return +end + + +""" +Trivial check of the status of the Lagrangian dual solution under lax regime. +""" +function lagrangian_status_check( + lag_status::Symbol, + dual_status_regime::DynamicSDDiP.Lax, + ) + + return +end + + +""" +Initializing duals with zero vector. +""" +function initialize_duals( + node::SDDP.Node, + subproblem::JuMP.Model, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + dual_initalization_regime::DynamicSDDiP.ZeroDuals, +) + + # Get number of states and create zero vector for duals + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + dual_vars_initial = zeros(number_of_states) + + return + +end + + +""" +Initializing duals by solving LP relaxation. +""" +function initialize_duals( + node::SDDP.Node, + subproblem::JuMP.Model, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + dual_initalization_regime::DynamicSDDiP.LPDuals, +) + + # Get number of states and create zero vector for duals + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + dual_vars_initial = zeros(number_of_states) + + # Create LP Relaxation + undo_relax = JuMP.relax_integrality(subproblem); + + # Define appropriate solver + set_solver(subproblem, algo_params, applied_solvers, :LP_relax) + + # Solve LP Relaxation + JuMP.optimize!(subproblem) + + # Get dual values (reduced costs) for binary states as initial solution #TODO + get_and_set_dual_values!(node, dual_vars_initial, algo_params.state_approximation_regime) + + # Undo relaxation + undo_relax() + + return + +end + +function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Float64}, + state_approximation_regime::DynamicSDDiP.BinaryApproximation) + + for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) + variable_name = node.ext[:backward_data][:bin_states][name] + reference_to_constr = FixRef(variable_name) + dual_vars_initial[i] = JuMP.getdual(reference_to_constr) + end + + return +end + +function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Float64}, + state_approximation_regime::DynamicSDDiP.NoState) + + for (i, name) in enumerate(keys(node,states)) + reference_to_constr = FixRef(name) + dual_vars_initial[i] = JuMP.getdual(reference_to_constr) + end + + return +end + +function store_dual_values!(node::SDDP.Node, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, + integrality_handler::SDDP.SDDiP, state_approximation_regime::DynamicSDDiP.BinaryApproximation) + + for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) + dual_values[name] = dual_vars[i] + + value = integrality_handler.old_rhs[i] + x_name = node.ext[:backward_data][:bin_x_names][name] + k = node.ext[:backward_data][:bin_k][name] + bin_state[name] = BinaryState(value, x_name, k) + end + + return +end + +function store_dual_values!(node::SDDP.Node, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, + integrality_handler::SDDP.SDDiP, state_approximation_regime::DynamicSDDiP.NoStateApproximation) + + for (i, name) in enumerate(keys(node.states)) + dual_values[name] = dual_vars[i] + end + + return +end diff --git a/src/lagrange.jl b/src/lagrange.jl index a915e452..1aa7e1ec 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -42,359 +42,392 @@ ################################################################################ +""" +Solving the Lagrangian relaxation problem, i.e. the inner problem of the +Lagrangian dual +""" + +function _solve_Lagrangian_relaxation!( + node::SDDP.Node, + π_k::Vector{Float64}, + h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, + h_k::Vector{Float64}, + update_subgradients::Bool = true, +) + model = node.subproblem + + # Set the Lagrangian relaxation of the objective in the primal model + old_obj = JuMP.objective_function(model) + JuMP.set_objective_function(model, @expression(old_obj - π_k' * h_expr)) + + # Optimization + JuMP.optimize!(model) + @assert JuMP.termination_status(model) == MOI.OPTIMAL + + # Update the correct values + L_k = JuMP.objective_value(model) + if update_subgradients + h_k .= -JuMP.value.(h_expr) + end + + # Reset old objective + JuMP.set_objective_function(model, old_obj) + + return L_k +end + + """ Kelley's method to solve Lagrangian dual """ -function _kelley( +function solve_lagrangian_dual( node::SDDP.Node, node_index::Int64, - obj::Float64, - dual_vars::Vector{Float64}, - integrality_handler::SDDP.SDDiP, + primal_obj::Float64, + π_k::Vector{Float64}, + bound_results::Tuple{Float64,Float64} algo_params::NCNBD.AlgoParams, applied_solvers::NCNBD.AppliedSolvers, - dual_bound::Union{Float64,Nothing} + dual_solution_regime::DynamicSDDiP.Kelley ) + ############################################################################ # INITIALIZATION ############################################################################ - atol = integrality_handler.atol - rtol = integrality_handler.rtol - model = node.ext[:linSubproblem] - # Assume the model has been solved. Solving the MIP is usually very quick - # relative to solving for the Lagrangian duals, so we cheat and use the - # solved model's objective as our bound while searching for the optimal duals + # A sign bit that is used to avoid if-statements in the models (see SDDP.jl) + s = JuMP.objective_sense(node.subproblem) == MOI.MIN_SENSE ? 1 : -1 + + # Storage for the cutting-plane method + #--------------------------------------------------------------------------- + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + # The original value for x (former old_rhs) + x_in_value = zeros(number_of_states) + # The current estimate for π (in our case determined in initialization) + # π_k + # The best estimate for π (former best_mult) + π_star = zeros(number_of_states) + # The best estimate for the dual objective value (ignoring optimization sense) + L_star = -Inf + # The expression for ̄x-z (former slacks) + h_expr = Vector{AffExpr}(undef, number_of_states) + # The current value of ̄x-z (former subgradients) + h_k = zeros(number_of_states) + + # Set tolerances + #--------------------------------------------------------------------------- + atol = algo_params.duality_regime.atol + rtol = algo_params.duality_regime.rtol + iteration_limit = algo_params.duality_regime.iteration_limit + + # Set solver for inner problem + #--------------------------------------------------------------------------- + set_solver(node.subproblem, algo_params, applied_solvers, :lagrange_relax) - # This does not work since the problem has been changed since then - #assert JuMP.termination_status(model) == MOI.OPTIMAL - #obj = JuMP.objective_value(model) - - for (i, (name, bin_state)) in enumerate(node.ext[:backward_data][:bin_states]) - integrality_handler.old_rhs[i] = JuMP.fix_value(bin_state) - integrality_handler.slacks[i] = bin_state - integrality_handler.old_rhs[i] - JuMP.unfix(bin_state) - #JuMP.unset_binary(state_comp.in) # TODO: maybe not required - JuMP.set_lower_bound(bin_state, 0) - JuMP.set_upper_bound(bin_state, 1) - end + ############################################################################ + # RELAXING THE COPY CONSTRAINTS + ############################################################################ + relax_copy_constraints(node, x_in_value, h_expr, algo_params.state_approximation_regime) + ############################################################################ # LOGGING OF LAGRANGIAN DUAL ############################################################################ - lag_log_file_handle = open("C:/Users/cg4102/Documents/julia_logs/Lagrange.log", "a") - print_helper(print_lagrange_header, lag_log_file_handle) + #lag_log_file_handle = open("C:/Users/cg4102/Documents/julia_logs/Lagrange.log", "a") + #print_helper(print_lagrange_header, lag_log_file_handle) - # SET-UP APPROXIMATION MODEL ############################################################################ - # Subgradient at current solution - subgradients = integrality_handler.subgradients - # Best multipliers found so far - best_mult = integrality_handler.best_mult - # Dual problem has the opposite sense to the primal - dualsense = ( - JuMP.objective_sense(model) == JuMP.MOI.MIN_SENSE ? JuMP.MOI.MAX_SENSE : - JuMP.MOI.MIN_SENSE - ) - - # Approximation of Lagrangian dual as a function of the multipliers - approx_model = JuMP.Model(Gurobi.Optimizer) - - if applied_solvers.Lagrange == "CPLEX" - set_optimizer(approx_model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - elseif applied_solvers.Lagrange == "Gurobi" - set_optimizer(approx_model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "NumericFocus"=>1)) - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - else - set_optimizer(approx_model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0)) - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - end - - # Objective estimate and Lagrangian duals - @variables approx_model begin - θ - x[1:length(dual_vars)] - end - JuMP.@objective(approx_model, dualsense, θ) - - if dualsense == MOI.MIN_SENSE - JuMP.set_lower_bound(θ, obj) - (best_actual, f_actual, f_approx) = (Inf, Inf, -Inf) - else - #JuMP.set_upper_bound(θ, 10000.0) - - JuMP.set_upper_bound(θ, obj) - (best_actual, f_actual, f_approx) = (-Inf, -Inf, Inf) - end - - # BOUND DUAL VARIABLES IF INTENDED + # SET-UP THE APPROXIMATING CUTTING-PLANE MODEL ############################################################################ - if !isnothing(dual_bound) - for i in 1:length(dual_vars) - JuMP.set_lower_bound(x[i], -dual_bound) - JuMP.set_upper_bound(x[i], dual_bound) - end - - if dualsense == MOI.MIN_SENSE - JuMP.set_lower_bound(θ, -Inf) - else - JuMP.set_upper_bound(θ, Inf) - end - end + # Approximation of Lagrangian dual by cutting planes + # Optimizer is re-set anyway + approx_model = JuMP.Model(GLPK.Optimizer) + set_solver(approx_model, algo_params, applied_solvers, :kelley) + + # Create the objective + # Note that it is always formulated as a maximization problem, but that + # s modifies the sense appropriately + @variable(approx_model, t) + set_objective_bound(approx_model, s, bound_results.obj_bound) + @objective(approx_model, Max, t) + + # Create the dual variables + # Note that the real dual multipliers are split up into two non-negative + # variables here, which is required for the Magnanti Wong part later + @variable(approx_model, π⁺[1:number_of_states] >= 0) + @variable(approx_model, π⁻[1:number_of_states] >= 0) + @expression(approx_model, π, π⁺ .- π⁻) # not required to be a constraint + set_multiplier_bounds(approx_model, number_of_states bound_results.dual_bound) + ############################################################################ # CUTTING-PLANE METHOD ############################################################################ iter = 0 lag_status = :none - while iter < integrality_handler.iteration_limit + # set up optimal value of approx_model (former f_approx) + t_k = -Inf + + while iter <= iteration_limit && !isapprox(L_star, t_k, atol = atol, rtol = rtol) iter += 1 + ######################################################################## # SOLVE LAGRANGIAN RELAXATION FOR GIVEN DUAL_VARS ######################################################################## - # Evaluate the real function and a subgradient - f_actual = _solve_Lagrangian_relaxation!(subgradients, node, dual_vars, integrality_handler.slacks, :yes) - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #|| model.ext[:sddp_policy_graph].ext[:iteration] == 12 + # Evaluate the inner problem and determine a subgradient + L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] + ######################################################################## # ADD CUTTING PLANE ######################################################################## - # Update the model and update best function value so far - if dualsense == MOI.MIN_SENSE - JuMP.@constraint( - approx_model, - θ >= f_actual + LinearAlgebra.dot(subgradients, x - dual_vars) - ) - if f_actual <= best_actual - best_actual = f_actual - best_mult .= dual_vars - end - else - JuMP.@constraint( - approx_model, - θ <= f_actual + LinearAlgebra.dot(subgradients, x - dual_vars) - ) - if f_actual >= best_actual - best_actual = f_actual - best_mult .= dual_vars - end + JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) + + ######################################################################## + # UPDATE BEST FOUND SOLUTION SO FAR + ######################################################################## + if s * L_k >= L_star + L_star = s * L_k + π_star .= π_k end + ######################################################################## # SOLVE APPROXIMATION MODEL ######################################################################## # Get a bound from the approximate model JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL - f_approx = JuMP.objective_value(approx_model) - - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #|| model.ext[:sddp_policy_graph].ext[:iteration] == 8 + t_k = JuMP.objective_value(approx_model) + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - print("UB: ", f_approx, ", LB: ", f_actual) - println() + #print("UB: ", f_approx, ", LB: ", f_actual) + #println() - # CONVERGENCE CHECKS AND UPDATE ######################################################################## - # convergence achieved - if isapprox(best_actual, f_approx, atol = atol, rtol = rtol) - # convergence to obj -> tight cut - if isapprox(best_actual, obj, atol = atol, rtol = rtol) - lag_status = :aopt - # convergence to a smaller value than obj - # maybe possible due to numerical issues - # -> valid cut - else - lag_status = :conv - end - - # zero subgradients (and no further improvement), despite no convergence - # maybe possible due to numerical issues - # -> valid cut - elseif all(subgradients.==0) - lag_status = :sub + # PREPARE NEXT ITERATION + ######################################################################## + # dual_vars .= value.(x) + # can be deleted with the next update of GAMS.jl + # replace!(dual_vars, NaN => 0) - # lb exceeds ub: no convergence - elseif best_actual > f_approx + atol/10.0 + if L_star > t_k + atol/10.0 error("Could not solve for Lagrangian duals. LB > UB.") end + end - # return - if lag_status == :sub || lag_status == :aopt || lag_status == :conv - dual_vars .= best_mult - if dualsense == JuMP.MOI.MIN_SENSE - dual_vars .*= -1 - end - - for (i, (name, bin_state)) in enumerate(node.ext[:backward_data][:bin_states]) - #prepare_state_fixing!(node, state_comp) - JuMP.fix(bin_state, integrality_handler.old_rhs[i], force = true) - end + ############################################################################ + # CONVERGENCE ANALYSIS + ############################################################################ + if isapprox(L_star, t_k, atol = atol, rtol = rtol) + # CONVERGENCE ACHIEVED + if isapprox(L_star, s * primal_obj, atol = atol, rtol = rtol) + # CONVERGENCE TO TRUE OPTIMUM (APPROXIMATELY) + lag_status = :opt + else + # CONVERGENCE TO A SMALLER VALUE THAN THE PRIMAL OBJECTIVE + # sometimes this occurs due to numerical issues + # still leads to a valid cut + lag_status = :conv + elseif all(h_k .== 0) + # NO OPTIMALITY ACHIEVED, BUT STILL ALL SUBGRADIENTS ARE ZERO + # may occur due to numerical issues + lag_status = :sub + elseif iter == iteration_limit + # TERMINATION DUE TO ITERATION LIMIT + # stil leads to a valid cut + lag_status = :iter + end - if applied_solvers.MILP == "CPLEX" - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.MILP, "optcr"=>0.0, "numericalemphasis"=>0)) - elseif applied_solvers.MILP == "Gurobi" - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.MILP, "optcr"=>0.0, "NumericFocus"=>1)) - else - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.MILP, "optcr"=>0.0)) - end + ############################################################################ + # APPLY MAGNANTI AND WONG APPROACH IF INTENDED + ############################################################################ + magnanti_wong(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, + iteration_limit, atol, rtol, algo_params.dual_choice_regime) - return (lag_obj = best_actual, iterations = iter, lag_status = lag_status) - end + ############################################################################ + # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) + ############################################################################ + restore_copy_constraints(node, x_in_value, algo_params.state_approximation_regime) - # PREPARE NEXT ITERATION - ######################################################################## - # Next iterate - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #|| model.ext[:sddp_policy_graph].ext[:iteration] == 8 - dual_vars .= value.(x) - # can be deleted with the next update of GAMS.jl - replace!(dual_vars, NaN => 0) + ############################################################################ + # RESET SOLVER + ############################################################################ + set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) - # Logging - print_helper(print_lag_iteration, lag_log_file_handle, iter, f_approx, best_actual, f_actual) + ############################################################################ + # LOGGING + ############################################################################ + print_helper(print_lag_iteration, lag_log_file_handle, iter, t_k, L_star, L_k) - end + # Set dual_vars (here π_k) to the optimal solution + π_k = π_star - lag_status = :iter - #error("Could not solve for Lagrangian duals. Iteration limit exceeded.") - return (lag_obj = best_actual, iterations = iter, lag_status = lag_status) + return (lag_obj = s * L_star, iterations = iter, lag_status = lag_status) end -""" -Solving the Lagrangian relaxation problem -""" -function _solve_Lagrangian_relaxation!( - subgradients::Vector{Float64}, + +function relax_copy_constraints!( node::SDDP.Node, - dual_vars::Vector{Float64}, - slacks, - update_subgradients::Symbol, #TODO: Why not boolean? -) - model = node.ext[:linSubproblem] - old_obj = JuMP.objective_function(model) - # Set the Lagrangian relaxation of the objective in the primal model - fact = (JuMP.objective_sense(model) == JuMP.MOI.MIN_SENSE ? 1 : -1) - new_obj = old_obj + fact * LinearAlgebra.dot(dual_vars, slacks) - JuMP.set_objective_function(model, new_obj) - JuMP.optimize!(model) - lagrangian_obj = JuMP.objective_value(model) + x_in_value::Vector{Float64}, + h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, + state_approximation_regime::DynamicSDDiP.BinaryApproximation + ) - if update_subgradients == :yes - subgradients .= fact .* JuMP.value.(slacks) - end + for (i, (_, state)) in enumerate(node.ext[:backward_data][:bin_states]) + # Store original value of ̄x, which z was fixed to + x_in_value[i] = JuMP.fix_value(bin_state) + # Store expression for slack + h_expr[i] = @expression(node.subproblem, bin_state - x_in_value[i]) + # Relax copy constraint (i.e. z does not have to take the value of ̄x anymore) + JuMP.unfix(bin_state) - # Reset old objective, update subgradients using slack values - JuMP.set_objective_function(model, old_obj) + # Set bounds to ensure that inner problems are feasible + # As we use binary approximation, 0 and 1 can be used + JuMP.set_lower_bound(bin_state, 0) + JuMP.set_upper_bound(bin_state, 1) - return lagrangian_obj + end + + return end +function relax_copy_constraints!( + node::SDDP.Node, + x_in_value::Vector{Float64}, + h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, + state_approximation_regime::DynamicSDDiP.NoStateApproximation + ) + + for (i, (_, state)) in enumerate(node.states) + # Store original value of ̄x, which z was fixed to + x_in_value[i] = JuMP.fix_value(state.in) + # Store expression for slack + h_expr[i] = @expression(node.subproblem, state.in - x_in_value[i]) + # Relax copy constraint (i.e. z does not have to take the value of ̄x anymore) + JuMP.unfix(state.in) + + # Set bounds to ensure that inner problems are feasible + # Bound shouldn't be too tight, so use 1e9 as a default if nothing + # else is specified + lb = has_lower_bound(state.out) ? lower_bound(state.out) : -1e9 + ub = has_upper_bound(state.out) ? upper_bound(state.out) : 1e9 + JuMP.set_lower_bound(state.in, lb) + JuMP.set_upper_bound(state.in, ub) + end -""" -Initializing duals. -""" -function initialize_duals( + return +end + +function restore_copy_constraints( node::SDDP.Node, - linearizedSubproblem::JuMP.Model, - dual_regime::Symbol, -) + x_in_value::Vector{Float64} + state_approximation_regime::DynamicSDDiP.BinaryApproximation, + ) - # Get number of states and create zero vector for duals - number_of_states = length(node.ext[:backward_data][:bin_states]) - dual_vars_initial = zeros(number_of_states) + for (i, (_, bin_state)) in enumerate(node.ext[:backward_data][:bin_states]) + #prepare_state_fixing!(node, state_comp) + JuMP.fix(bin_state, x_in_value[i], force = true) + end +end - # DUAL REGIME I: USE ZEROS - ############################################################################ - if dual_regime == :zeros - # Do nothing, since zeros are already defined +function restore_copy_constraints( + node::SDDP.Node, + x_in_value::Vector{Float64} + state_approximation_regime::DynamicSDDiP.NoStateApproximation, + ) - # DUAL REGIME II: USE LP RELAXATION - ############################################################################ - elseif dual_regime == :gurobi_relax || dual_regime == :cplex_relax - # Create LP Relaxation - undo_relax = JuMP.relax_integrality(linearizedSubproblem); - - # Define appropriate solver - if dual_regime == :gurobi_relax - set_optimizer(linearizedSubproblem, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>"Gurobi", "optcr"=>0.0)) - elseif dual_regime == :cplex_relax - set_optimizer(linearizedSubproblem, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>"CPLEX", "optcr"=>0.0)) - end + for (i, (_, state)) in enumerate(node.states) + #prepare_state_fixing!(node, state_comp) + JuMP.fix(state.in, x_in_value[i], force = true) + end +end - # Solve LP Relaxation - JuMP.optimize!(linearizedSubproblem) +function set_objective_bound(approx_model::JuMP.Model, s::Int, obj_bound::Float64) - # Get dual values (reduced costs) for binary states as initial solution - for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) - variable_name = node.ext[:backward_data][:bin_states][name] - reference_to_constr = FixRef(variable_name) - dual_vars_initial[i] = JuMP.getdual(reference_to_constr) - end + JuMP.set_upper_bound(approx_model[:t], s * obj_bound) - # Undo relaxation - undo_relax() +end - # DUAL REGIME III: USE FIXED MIP MODEL (DUALS ONLY PROVIDED BY CPLEX) - ############################################################################ - elseif dual_regime == :cplex_fixed - # Define cplex solver - set_optimizer(linearizedSubproblem, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>"CPLEX", "optcr"=>0.0)) - - # Solve original primal model in binary space - JuMP.optimize!(linearizedSubproblem) - - # Get dual values (reduced costs) for binary states as initial solution - for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) - variable_name = node.ext[:backward_data][:bin_states][name] - reference_to_constr = FixRef(variable_name) - dual_vars_initial[i] = JuMP.getdual(reference_to_constr) - end +function set_multiplier_bounds(approx_model::JuMP.Model, number_of_states::Int, dual_bound::Float64) - # DUAL REGIME IV: USE COMBINATION FOR CPLEX - # use fixed MIP model values for continuous original states - # use LP relaxation values for binary or integer original states - ############################################################################ - elseif dual_regime == :cplex_combi - # Define cplex solver - set_optimizer(linearizedSubproblem, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>"CPLEX", "optcr"=>0.0)) - - # Solve original primal model in binary space - JuMP.optimize!(linearizedSubproblem) - - # Get dual values (reduced costs) for binary states as initial solution - for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) - variable_name = node.ext[:backward_data][:bin_states][name] - reference_to_constr = FixRef(variable_name) - dual_vars_initial[i] = JuMP.getdual(reference_to_constr) - end + for i in 1:number_of_states + JuMP.set_upper_bound(π⁺[i], dual_bound) + JuMP.set_upper_bound(π⁻[i], dual_bound) + end +end - # Create LP Relaxation - undo_relax = JuMP.relax_integrality(linearizedSubproblem); +""" +Given the optimal dual objective value from the Kelley's method, try to find +the optimal dual multipliers with the smallest L1-norm. - # Solve LP Relaxation - JuMP.optimize!(linearizedSubproblem) +Note that this is only done until the maximum number of iterations is +achieved in total. +""" - # Replace dual values for binary and integer original variables - for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) - variable_name = node.ext[:backward_data][:bin_states][name] - reference_to_constr = FixRef(variable_name) +function magnanti_wong( + node::SDDP.Node, + approx_model::JuMP.model, + π_k::Vector{Float64}, + π_star::Vector{Float64}, + t_k::Float64, + h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, + h_k::Vector{Float64}, + s::Int, + L_k::Float64, + L_star::Float64, + iteration_limit::Int, + atol::Float64, + rtol::Float64, + dual_choice_regime::DynamicSDDiP.MagnantiWongChoice, + ) - # associated original state - # TODO: Why is this stored in backward_data, but also in BinaryState struct itself? - original_state_sym = node.ext[:backward_data][:bin_x_names][name] - original_state = node.ext[:lin_states][original_state_sym] + # Reset objective + @objective(approx_model, Min, sum(π⁺) + sum(π⁻)) + JuMP.set_lower_bound(t, t_k) - # if original state is integer or binary, replace dual_vars_initial - if original_state.info.in.binary || original_state.info.in.integer - dual_vars_initial[i] = JuMP.getdual(reference_to_constr) - end + # The worst-case scenario in this for-loop is that we run through the + # iterations without finding a new dual solution. However if that happens + # we can just keep our current λ_star. + for _ in (iter+1):iteration_limit + JuMP.optimize!(approx_model) + @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL + π_k .= value.(π) + L_k = _solve_Lagrangian_relaxation(node.subproblem, π_k, h_expr, h_k) + if isapprox(L_star, L_k, atol = atol, rtol = rtol) + # At this point we tried the smallest ‖π‖ from the cutting plane + # problem, and it returned the optimal dual objective value. No + # other optimal dual vector can have a smaller norm. + π_star = π_k + return end - - # Undo relaxation - undo_relax() + JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) end - return dual_vars_initial + return end +function magnanti_wong( + node::SDDP.Node, + approx_model::JuMP.model, + π_k::Vector{Float64}, + π_star::Vector{Float64}, + t_k::Float64, + h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, + h_k::Vector{Float64}, + s::Int, + L_k::Float64, + L_star::Float64, + iteration_limit::Int, + atol::Float64, + rtol::Float64, + dual_choice_regime::DynamicSDDiP.StandardChoice, + ) + + return +end + + + """ Level bundle method to solve the Lagrangian duals. """ diff --git a/src/logging.jl b/src/logging.jl index 8fcc15ed..4fe55f10 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -53,6 +53,7 @@ struct Options{T} similar_children::Dict{T,Vector{T}} start_time::Float64 log::Vector{DynamicSDDiP.Log} + log_file_handle::Any # Internal function: users should never construct this themselves. function Options( @@ -60,6 +61,7 @@ struct Options{T} initial_state::Dict{Symbol,Float64}, start_time::Float64, log::Vector{DynamicSDDiP.Log}, + log_file_handle::Any ) where {T} return new{T}( initial_state, @@ -68,6 +70,7 @@ struct Options{T} SDDP.get_same_children(model), start_time, log, + log_file_handle ) end end @@ -132,23 +135,23 @@ function print_parameters(io, algo_params::DynamicSDDiP.AlgoParams, applied_solv println(io, "------------------------------------------------------------------------") print(io, "Cut family used: ") - println(io, algo_params.cut_family_regime) - if algo_params.cut_family_regime == DynamicSDDiP.LagrangianCut - cut_family_regime = algo_params.cut_family_regime + println(io, algo_params.duality_regime) + if algo_params.duality_regime == DynamicSDDiP.LagrangianDuality + duality_regime = algo_params.duality_regime print(io, "Dual initialization: ") - println(io, cut_family_regime.dual_initialization_regime) + println(io, duality_regime.dual_initialization_regime) print(io, "Dual bounding: ") - println(io, cut_family_regime.dual_bound_regime) + println(io, duality_regime.dual_bound_regime) print(io, "Dual solution method: ") - println(io, cut_family_regime.dual_solution_regime) + println(io, duality_regime.dual_solution_regime) print(io, "Dual multiplier choice: ") - println(io, cut_family_regime.dual_choice_regime) + println(io, duality_regime.dual_choice_regime) print(io, "Dual status regime: ") - println(io, cut_family_regime.dual_status_regime) + println(io, duality_regime.dual_status_regime) #print(io, "Numerical focus used: ") - #println(io, cut_family_regime.numerical_focus) + #println(io, duality_regime.numerical_focus) println(io, "------------------------------------------------------------------------") - dual_solution_regime = cut_family_regime.dual_solution_regime + dual_solution_regime = duality_regime.dual_solution_regime println(io, Printf.@sprintf("Lagrangian rtol: %1.4e", dual_solution_regime.rtol)) println(io, Printf.@sprintf("Lagrangian atol: %1.4e", dual_solution_regime.atol)) println(io, Printf.@sprintf("iteration_limit: %5d", dual_solution_regime.iteration_limit)) @@ -253,9 +256,9 @@ function print_footer(io, training_results) flush(io) end -function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log) +function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log_file_handle::Any, log::DynamicSDDiP.log) if algo_params.print_level > 0 && mod(length(log), algo_params.log_frequency) == 0 - print_helper(print_iteration, algo_params.log_file_handle, log[end]) + print_helper(print_iteration, log_file_handle, log[end]) end end diff --git a/src/regularizations.jl b/src/regularizations.jl index f484e055..4b6779fe 100644 --- a/src/regularizations.jl +++ b/src/regularizations.jl @@ -68,6 +68,7 @@ function regularize_subproblem!(node::SDDP.Node, node_index::Int64, const_norm = JuMP.@constraint(subproblem, v >= sum(alpha[i] for i in 1:number_of_states)) push!(reg_data[:reg_constraints], const_norm) + return end """ @@ -108,6 +109,7 @@ function deregularize_subproblem!(node::SDDP.Node, subproblem::JuMP.Model, regul delete!(node.ext, :regularization_data) + return end """ @@ -119,9 +121,9 @@ end """ -Introducing a regularizing term to the backward pass problem in binary space. +Regularizing the backward pass problem in binary space if regularization is used. """ -function regularize_backward!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64) +function regularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64, regularization_regime::DynamicSDDiP.Regularization) bw_data = node.ext[:backward_data] binary_states = bw_data[:bin_states] @@ -134,6 +136,7 @@ function regularize_backward!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Fl reg_data[:reg_variables] = JuMP.VariableRef[] reg_data[:reg_constraints] = JuMP.ConstraintRef[] + ############################################################################ # DETERMINE SIGMA TO BE USED IN BINARY SPACE ############################################################################ Umax = 0 @@ -145,6 +148,7 @@ function regularize_backward!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Fl # Here, not sigma, but a different regularization parameter is used sigma_bin = sigma * Umax + ############################################################################ # UNFIX THE STATE VARIABLES ############################################################################ for (i, (name, state_comp)) in enumerate(binary_states) @@ -154,10 +158,12 @@ function regularize_backward!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Fl follow_state_unfixing_binary!(state_comp) end + ############################################################################ # STORE ORIGINAL OBJECTIVE FUNCTION ############################################################################ old_obj = reg_data[:old_objective] = JuMP.objective_function(subproblem) + ############################################################################ # DEFINE NEW VARIABLES, CONSTRAINTS AND OBJECTIVE ############################################################################ # These variables and constraints are used to define the norm of the slack as a MILP @@ -188,17 +194,29 @@ function regularize_backward!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Fl const_norm = JuMP.@constraint(subproblem, v >= sum(alpha[i] for i in 1:number_of_states)) push!(reg_data[:reg_constraints], const_norm) + return +end + +""" +Trivial regularization of the backward pass problem in binary space +if no regularization is used. +""" +function regularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64, regularization_regime::DynamicSDDiP.NoRegularization) + + return end """ -Regaining the unregularized problem in binary space. +Regaining the unregularized problem in binary space if regularization +was used. """ -function deregularize_backward!(node::SDDP.Node, subproblem::JuMP.Model) +function deregularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.Regularization) reg_data = node.ext[:regularization_data] bw_data = node.ext[:backward_data] + ############################################################################ # FIX THE STATE VARIABLES ############################################################################ for (i, (name, state_comp)) in enumerate(bw_data[:bin_states]) @@ -206,10 +224,12 @@ function deregularize_backward!(node::SDDP.Node, subproblem::JuMP.Model) JuMP.fix(state_comp, reg_data[:fixed_state_value][name], force=true) end + ############################################################################ # REPLACE THE NEW BY THE OLD OBJECTIVE ############################################################################ JuMP.set_objective_function(subproblem, reg_data[:old_objective]) + ############################################################################ # DELETE ALL REGULARIZATION-BASED VARIABLES AND CONSTRAINTS ############################################################################ delete(subproblem, reg_data[:reg_variables]) @@ -220,4 +240,15 @@ function deregularize_backward!(node::SDDP.Node, subproblem::JuMP.Model) delete!(node.ext, :regularization_data) + return +end + + +""" +Trivial regaining of the unregularized problem in binary space if no regularization +was used. +""" +function deregularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.NoRegularization) + + return end diff --git a/src/solverHandling.jl b/src/solverHandling.jl index 6a7892ab..345eb538 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -23,6 +23,16 @@ function set_solver( else solver = applied_solvers.MILP end + elseif algorithmic_step in [:lagrange_relax] + if algo_params.cut_projection_method == :Bilinear + solver = applied_solvers.MINLP + else + solver = applied_solvers.lagrange + end + elseif algorithmic_step in [:level_bundle] + solver = applied_solvers.NLP + elseif algorithmic_step in [:LP_relax, :kelley] + solver = applied_solvers.LP end # CHECK NUMERICAL FOCUS @@ -61,3 +71,6 @@ function set_solver( end end + + return +end diff --git a/src/typedefs.jl b/src/typedefs.jl index b5e1aa35..44227e46 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -56,7 +56,7 @@ Default is ZeroDuals. mutable struct ZeroDuals <: AbstractDualInitializationRegime end mutable struct LPDuals <: AbstractDualInitializationRegime end -mutable struct CPLEXFixed <: AbstractDualInitializationRegime end +#mutable struct CPLEXFixed <: AbstractDualInitializationRegime end ################################################################################ # SOLUTION METHOD FOR LAGRANGIAN DUAL @@ -72,34 +72,35 @@ Default is Kelley. """ mutable struct Kelley <: AbstractDualSolutionRegime - atol::Float64 - rtol::Float64 - iteration_limit::Int - function LevelBundle(; - atol = 1e-8, - rtol = 1e-8, - iteration_limit = 1000, - ) - return new(atol, rtol, iteration_limit) - end + # atol::Float64 + # rtol::Float64 + # iteration_limit::Int + # function Kelley(; + # atol = 1e-8, + # rtol = 1e-8, + # iteration_limit = 1000, + # ) + # return new(atol, rtol, iteration_limit) + # end end mutable struct LevelBundle <: AbstractDualSolutionRegime - atol::Float64 - rtol::Float64 - iteration_limit::Int + # atol::Float64 + # rtol::Float64 + # iteration_limit::Int bundle_alpha::Float64 bundle_factor::Float64 level_factor::Float64 function LevelBundle(; - atol = 1e-8, - rtol = 1e-8, - iteration_limit = 1000, + # atol = 1e-8, + # rtol = 1e-8, + # iteration_limit = 1000, bundle_alpha = 1.0, bundle_factor = 1.0, level_factor = 1.0, ) - return new(atol, rtol, iteration_limit, bundle_alpha, bundle_factor, level_factor) + #return new(atol, rtol, iteration_limit, bundle_alpha, bundle_factor, level_factor) + return new(bundle_alpha, bundle_factor, level_factor) end end @@ -242,39 +243,46 @@ Default is Regularization. ################################################################################ # CUT FAMILY TO BE USED ################################################################################ -abstract type AbstractCutFamilyRegime end +abstract type AbstractDualityRegime end -mutable struct LagrangianCut <: AbstractCutFamilyRegime end +mutable struct LagrangianDuality <: AbstractDualityRegime end -mutable struct LagrangianCut <: AbstractCutFamilyRegime +mutable struct LagrangianDuality <: AbstractDualityRegime + atol::Float64 + rtol::Float64 + iteration_limit::Int dual_initialization_regime::AbstractDualInitializationRegime dual_bound_regime::AbstractDualBoundRegime dual_solution_regime::AbstractDualSolutionRegime dual_choice_regime::AbstractDualChoiceRegime dual_status_regime::AbstractDualStatusRegime function BinaryApproximation(; + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, dual_initialization_regime = ZeroDuals(), dual_bound_regime = BothBounds(), dual_solution_regime = Kelley(), dual_choice_regime = MagnantiWongChoice(), dual_status_regime = Rigorous(), ) - return new(dual_initialization_regime, dual_bound_regime, + return new(atol, rtol, iteration_limit, + dual_initialization_regime, dual_bound_regime, dual_solution_regime, dual_choice_regime, dual_status_regime) end end -mutable struct BendersCut <: AbstractCutFamilyRegime end -mutable struct StrengthenedCut <: AbstractCutFamilyRegime end +mutable struct LinearDuality <: AbstractDualityRegime end +mutable struct StrengthenedDuality <: AbstractDualityRegime end """ -LagrangianCut means that the Lagrangian dual is (approximately) solved to obtain +LagrangianDuality means that the Lagrangian dual is (approximately) solved to obtain a cut. In this case, a lot of parameters have to be defined to configure the solution of the Lagrangian dual. -BendersCut means that the LP relaxation is solved to obtain a cut. -StrengthenedCut means that the Lagrangian relaxation is solved using the optimal +LinearDuality means that the LP relaxation is solved to obtain a cut. +StrengthenedDuality means that the Lagrangian relaxation is solved using the optimal dual multiplier of the LP relaxation to obtain a strengthened Benders cuts. -Default is LagrangianCut. +Default is LagrangianDuality. """ ################################################################################ @@ -313,7 +321,7 @@ mutable struct AlgoParams stopping_rules::Vector{SDDP.AbstractStoppingRule} state_approximation_regime::AbstractStateApproximationRegime regularization_regime::AbstractRegularizationRegime - cut_family_regime::AbstractCutFamilyRegime + duality_regime::AbstractDualityRegime cut_selection_regime::AbstractCutSelectionRegime ############################################################################ risk_measure = SDDP.Expectation() @@ -335,7 +343,7 @@ mutable struct AlgoParams stopping_rules = [DeterministicStopping()], state_approximation_regime = BinaryApproximation(), regularization_regime = Regularization(), - cut_family_regime = LagrangianCut(), + duality_regime = LagrangianDuality(), cut_selection_regime = CutSelection(), forward_pass = SDDP.DefaultForwardPass(), sampling_scheme = SDDP.InSampleMonteCarlo(), @@ -354,7 +362,7 @@ mutable struct AlgoParams stopping_rules, state_approximation_regime, regularization_regime, - cut_family_regime, + duality_regime, cut_selection_regime, forward_pass, sampling_scheme, diff --git a/stuff.jl b/stuff.jl index 22c57e62..2e4fbe36 100644 --- a/stuff.jl +++ b/stuff.jl @@ -15,3 +15,149 @@ lim = SDDP.IterationLimit print(lim) print(typeof(lim)) print(lim == SDDP.IterationLimit) + + +function return_test() + + obj = 1000.0 + dual_bound = Inf + + return ( + obj, + dual_bound + ) + +end + +return_results = return_test() + +typeof(return_results) + + + +using JuMP +using GLPK +approx_model = JuMP.Model(GLPK.Optimizer) +@variable(approx_model, t <= 100) +@objective(approx_model, Max, t) + +# Create the dual variables +# Note that the real dual multipliers are split up into two non-negative +# variables here, which is required for the Magnanti Wong part later +@variable(approx_model, π⁺[1:5] >= 0) +approx_model[:π⁺] + + + + +function get_dual_solution(node::Node, lagrange::LagrangianDuality) + # Assume the model has been solved. Solving the MIP is usually very quick + # relative to solving for the Lagrangian duals, so we cheat and use the + # solved model's objective as our bound while searching for the optimal + # duals. + @assert JuMP.termination_status(node.subproblem) == MOI.OPTIMAL + # Query the current MIP solution here. This is used as a bound for the + # cutting plane method. + primal_obj = JuMP.objective_value(node.subproblem) + # A sign bit that is used to avoid if-statements in the models. + s = JuMP.objective_sense(node.subproblem) == MOI.MIN_SENSE ? 1 : -1 + # Storage for the cutting plane method. + num_states = length(node.states) + x_in_value = zeros(num_states) # The original value of x. + λ_k = zeros(num_states) # The current estimate for λ + λ_star = zeros(num_states) # The best estimate for λ + # The best estimate for the dual objective value, ignoring optimization + # sense (bigger is better). + L_star = -Inf + h_expr = Vector{AffExpr}(undef, num_states) # The expression for x̄ - x + h_k = zeros(num_states) # The value of x̄_k - x + # Start by relaxing the fishing constraint. + for (i, (_, state)) in enumerate(node.states) + # We're going to need this value later when we reset things. + x_in_value[i] = JuMP.fix_value(state.in) + h_expr[i] = @expression(node.subproblem, state.in - x_in_value[i]) + # Relax the constraint from the problem. + JuMP.unfix(state.in) + # We need bounds to ensure that the dual problem is feasible. However, + # they can't be too tight. Let's use 1e9 as a default... + lb = has_lower_bound(state.out) ? lower_bound(state.out) : -1e9 + ub = has_upper_bound(state.out) ? upper_bound(state.out) : 1e9 + JuMP.set_lower_bound(state.in, lb) + JuMP.set_upper_bound(state.in, ub) + end + # Create the model for the cutting plane algorithm + model = JuMP.Model(something(lagrange.optimizer, node.optimizer)) + @variable(model, λ⁺[1:num_states] >= 0) + @variable(model, λ⁻[1:num_states] >= 0) + @variable(model, t <= s * primal_obj) + @expression(model, λ, λ⁺ .- λ⁻) + @objective(model, Max, t) + # Step 1: find an optimal dual solution and corresponding objective value. + iter, t_k = 0, s * primal_obj + while !isapprox(L_star, t_k, atol = lagrange.atol, rtol = lagrange.rtol) + iter += 1 + if iter > lagrange.iteration_limit + error("Iteration limit exceeded in Lagrangian subproblem.") + end + JuMP.optimize!(model) + @assert JuMP.termination_status(model) == JuMP.MOI.OPTIMAL + t_k = JuMP.objective_value(model) + λ_k .= value.(λ) + L_k = _solve_primal_problem(node.subproblem, λ_k, h_expr, h_k) + JuMP.@constraint(model, t <= s * (L_k + h_k' * (λ .- λ_k))) + if s * L_k >= L_star + L_star = s * L_k + λ_star .= λ_k + end + end + # Step 2: given the optimal dual objective value, try to find the optimal + # dual solution with the smallest L1-norm ‖λ‖. + @objective(model, Min, sum(λ⁺) + sum(λ⁻)) + set_lower_bound(t, t_k) + # The worst-case scenario in this for-loop is that we run through the + # iterations without finding a new dual solution. However if that happens + # we can just keep our current λ_star. + for _ in (iter+1):lagrange.iteration_limit + JuMP.optimize!(model) + @assert JuMP.termination_status(model) == JuMP.MOI.OPTIMAL + λ_k .= value.(λ) + L_k = _solve_primal_problem(node.subproblem, λ_k, h_expr, h_k) + if isapprox(L_star, L_k, atol = lagrange.atol, rtol = lagrange.rtol) + # At this point we tried the smallest ‖λ‖ from the cutting plane + # problem, and it returned the optimal dual objective value. No + # other optimal dual vector can have a smaller norm. + λ_star = λ_k + break + end + JuMP.@constraint(model, t <= s * (L_k + h_k' * (λ .- λ_k))) + end + # Restore the fishing constraint x.in == x_in_value + for (i, (_, state)) in enumerate(node.states) + JuMP.fix(state.in, x_in_value[i], force = true) + end + λ_solution = Dict{Symbol,Float64}( + name => λ_star[i] for (i, name) in enumerate(keys(node.states)) + ) + # Remember to correct the sign of the optimal dual objective value. + return s * L_star, λ_solution +end + + +function _solve_primal_problem( + model::JuMP.Model, + λ::Vector{Float64}, + h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, + h_k::Vector{Float64}, +) + primal_obj = JuMP.objective_function(model) + JuMP.set_objective_function( + model, + @expression(model, primal_obj - λ' * h_expr), + ) + JuMP.optimize!(model) + @assert JuMP.termination_status(model) == MOI.OPTIMAL + h_k .= -JuMP.value.(h_expr) + L_λ = JuMP.objective_value(model) + JuMP.set_objective_function(model, primal_obj) + return L_λ +end From b8651e3526126fa0b6b5cde373787bdd47792392 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Tue, 12 Oct 2021 11:00:11 -0400 Subject: [PATCH 11/30] Implemented Level Bundle Method --- src/lagrange.jl | 343 ++++++++++++++++++++++-------------------------- stuff.jl | 3 + 2 files changed, 159 insertions(+), 187 deletions(-) diff --git a/src/lagrange.jl b/src/lagrange.jl index 1aa7e1ec..a5fa88c1 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -196,18 +196,13 @@ function solve_lagrangian_dual( JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL t_k = JuMP.objective_value(approx_model) + π_k .= JuMP.value(π) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #print("UB: ", f_approx, ", LB: ", f_actual) #println() ######################################################################## - # PREPARE NEXT ITERATION - ######################################################################## - # dual_vars .= value.(x) - # can be deleted with the next update of GAMS.jl - # replace!(dual_vars, NaN => 0) - if L_star > t_k + atol/10.0 error("Could not solve for Lagrangian duals. LB > UB.") end @@ -429,237 +424,211 @@ end """ -Level bundle method to solve the Lagrangian duals. +Level Bundle method to solve Lagrangian dual """ -function _bundle_level( +function solve_lagrangian_dual( node::SDDP.Node, node_index::Int64, - obj::Float64, - dual_vars::Vector{Float64}, - integrality_handler::SDDP.SDDiP, + primal_obj::Float64, + π_k::Vector{Float64}, + bound_results::Tuple{Float64,Float64} algo_params::NCNBD.AlgoParams, applied_solvers::NCNBD.AppliedSolvers, - dual_bound::Union{Float64,Nothing} + dual_solution_regime::DynamicSDDiP.LevelBundle ) + ############################################################################ # INITIALIZATION ############################################################################ - atol = integrality_handler.atol # corresponds to deltabar - rtol = integrality_handler.rtol # corresponds to deltabar - model = node.ext[:linSubproblem] - # Assume the model has been solved. Solving the MIP is usually very quick - # relative to solving for the Lagrangian duals, so we cheat and use the - # solved model's objective as our bound while searching for the optimal duals + # A sign bit that is used to avoid if-statements in the models (see SDDP.jl) + s = JuMP.objective_sense(node.subproblem) == MOI.MIN_SENSE ? 1 : -1 - # initialize bundle parameters - level_factor = algo_params.level_factor + # Storage for the cutting-plane method + #--------------------------------------------------------------------------- + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + # The original value for x (former old_rhs) + x_in_value = zeros(number_of_states) + # The current estimate for π (in our case determined in initialization) + # π_k + # The best estimate for π (former best_mult) + π_star = zeros(number_of_states) + # The best estimate for the dual objective value (ignoring optimization sense) + L_star = -Inf + # The expression for ̄x-z (former slacks) + h_expr = Vector{AffExpr}(undef, number_of_states) + # The current value of ̄x-z (former subgradients) + h_k = zeros(number_of_states) - for (i, (name, bin_state)) in enumerate(node.ext[:backward_data][:bin_states]) - integrality_handler.old_rhs[i] = JuMP.fix_value(bin_state) - integrality_handler.slacks[i] = bin_state - integrality_handler.old_rhs[i] - JuMP.unfix(bin_state) - #JuMP.unset_binary(state_comp.in) # TODO: maybe not required - JuMP.set_lower_bound(bin_state, 0) - JuMP.set_upper_bound(bin_state, 1) - end + # Set tolerances + #--------------------------------------------------------------------------- + atol = algo_params.duality_regime.atol + rtol = algo_params.duality_regime.rtol + iteration_limit = algo_params.duality_regime.iteration_limit - # LOGGING OF LAGRANGIAN DUAL - ############################################################################ - lag_log_file_handle = open("C:/Users/cg4102/Documents/julia_logs/Lagrange.log", "a") - print_helper(print_lagrange_header, lag_log_file_handle) + # Set solver for inner problem + #--------------------------------------------------------------------------- + set_solver(node.subproblem, algo_params, applied_solvers, :lagrange_relax) + + # Set bundle_parameters + #--------------------------------------------------------------------------- + level_factor = dual_solution_regime.level_factor + bundle_alpha = dual_solution.bundle_alpha + bundle_factor = dual_solution.bundle_factor - # SET-UP APPROXIMATION MODEL ############################################################################ - # Subgradient at current solution - subgradients = integrality_handler.subgradients - # Best multipliers found so far - best_mult = integrality_handler.best_mult - # Dual problem has the opposite sense to the primal - dualsense = ( - JuMP.objective_sense(model) == JuMP.MOI.MIN_SENSE ? JuMP.MOI.MAX_SENSE : - JuMP.MOI.MIN_SENSE - ) + # RELAXING THE COPY CONSTRAINTS + ############################################################################ + relax_copy_constraints(node, x_in_value, h_expr, algo_params.state_approximation_regime) - # Approximation of Lagrangian dual as a function of the multipliers - approx_model = JuMP.Model(Gurobi.Optimizer) - # even if objective is quadratic, it should be possible to use Gurobi - if applied_solvers.Lagrange == "CPLEX" - set_optimizer(approx_model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - elseif applied_solvers.Lagrange == "Gurobi" - set_optimizer(approx_model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "NumericFocus"=>1)) - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - else - set_optimizer(approx_model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0)) - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.Lagrange, "optcr"=>0.0, "numericalemphasis"=>0)) - end + ############################################################################ + # LOGGING OF LAGRANGIAN DUAL + ############################################################################ + #lag_log_file_handle = open("C:/Users/cg4102/Documents/julia_logs/Lagrange.log", "a") + #print_helper(print_lagrange_header, lag_log_file_handle) - # Define Lagrangian dual multipliers - @variables approx_model begin - θ - x[1:length(dual_vars)] - end + ############################################################################ + # SET-UP THE APPROXIMATING CUTTING-PLANE MODEL + ############################################################################ + # Approximation of Lagrangian dual by cutting planes + # Optimizer is re-set anyway + approx_model = JuMP.Model(GLPK.Optimizer) - if dualsense == MOI.MIN_SENSE - JuMP.set_lower_bound(θ, obj) - (best_actual, f_actual, f_approx) = (Inf, Inf, -Inf) - else - #JuMP.set_upper_bound(θ, 10000.0) + # Create the objective + # Note that it is always formulated as a maximization problem, but that + # s modifies the sense appropriately + @variable(approx_model, t) + set_objective_bound(approx_model, s, bound_results.obj_bound) + @objective(approx_model, Max, t) - JuMP.set_upper_bound(θ, obj) - (best_actual, f_actual, f_approx) = (-Inf, -Inf, Inf) - end + # Create the dual variables + # Note that the real dual multipliers are split up into two non-negative + # variables here, which is required for the Magnanti Wong part later + @variable(approx_model, π⁺[1:number_of_states] >= 0) + @variable(approx_model, π⁻[1:number_of_states] >= 0) + @expression(approx_model, π, π⁺ .- π⁻) # not required to be a constraint + set_multiplier_bounds(approx_model, number_of_states bound_results.dual_bound) - # BOUND DUAL VARIABLES IF INTENDED ############################################################################ - if !isnothing(dual_bound) - for i in 1:length(dual_vars) - JuMP.set_lower_bound(x[i], -dual_bound) - JuMP.set_upper_bound(x[i], dual_bound) - end - end - # CUTTING-PLANE METHOD ############################################################################ iter = 0 lag_status = :none - while iter < integrality_handler.iteration_limit + + # set up optimal value of approx_model (former f_approx) + t_k = -Inf + + while iter <= iteration_limit && !isapprox(L_star, t_k, atol = atol, rtol = rtol) iter += 1 + ######################################################################## # SOLVE LAGRANGIAN RELAXATION FOR GIVEN DUAL_VARS ######################################################################## - # Evaluate the real function and determine a subgradient - f_actual = _solve_Lagrangian_relaxation!(subgradients, node, dual_vars, integrality_handler.slacks, :yes) - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #|| model.ext[:sddp_policy_graph].ext[:iteration] == 12 + # Evaluate the inner problem and determine a subgradient + L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - # ADD CUTTING PLANE TO APPROX_MODEL ######################################################################## - # Update the model and update best function value so far - if dualsense == MOI.MIN_SENSE - JuMP.@constraint( - approx_model, - θ >= f_actual + LinearAlgebra.dot(subgradients, x - dual_vars) - # TODO: Reset upper bound to inf? - ) - if f_actual <= best_actual - best_actual = f_actual - best_mult .= dual_vars - end - else - JuMP.@constraint( - approx_model, - θ <= f_actual + LinearAlgebra.dot(subgradients, x - dual_vars) - # TODO: Reset lower boumd to -inf? - ) - if f_actual >= best_actual - # bestmult is not simply getvalue.(x), since approx_model may just haven gotten lucky - # same for best_actual - best_actual = f_actual - best_mult .= dual_vars - end + # ADD CUTTING PLANE + ######################################################################## + JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) + + ######################################################################## + # UPDATE BEST FOUND SOLUTION SO FAR + ######################################################################## + if s * L_k >= L_star + L_star = s * L_k + π_star .= π_k end - # SOLVE APPROXIMATION MODEL ######################################################################## - # Define objective for approx_model - JuMP.@objective(approx_model, dualsense, θ) + # RESET OBJECTIVE FOR APPROX_MODEL AFTER NONLINEAR MODEL + ######################################################################## + JuMP.@objective(approx_model, Max, t) + set_solver(approx_model, algo_params, applied_solvers, :kelley) - # Get an upper bound from the approximate model - # (we could actually also use obj here) + ######################################################################## + # SOLVE APPROXIMATION MODEL + ######################################################################## + # Get a bound from the approximate model JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL - f_approx = JuMP.objective_value(approx_model) + t_k = JuMP.objective_value(approx_model) + π_k .= JuMP.value(π) + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #|| model.ext[:sddp_policy_graph].ext[:iteration] == 12 + #print("UB: ", f_approx, ", LB: ", f_actual) + #println() - # Construct the gap (not directly used for termination, though) - #gap = abs(best_actual - f_approx) - gap = abs(best_actual - obj) + ######################################################################## + # COMPUTE GAP AND FORM A NEW LEVEL + ######################################################################## + gap = abs(L_k - primal_obj) #t_k + level = t_k - gap * level_factor + #TODO: - atol/10.0 for numerical issues? + JuMP.setlowerbound(t, level) - print("UB: ", f_approx, ", LB: ", f_actual, best_actual) - println() + ######################################################################## + # DETERMINE NEXT ITERATION USING PROXIMAL PROBLEM + ######################################################################## + # Objective function of approx model has to be adapted to new center + JuMP.@objective(approx_model, Min, sum((π_k[i] - π[i])^2 for i in 1:number_of_states)) + set_solver(approx_model, algo_params, applied_solvers, :level_bundle) + JuMP.optimize!(approx_model) + @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL - # CONVERGENCE CHECKS AND UPDATE ######################################################################## - # convergence achieved - if isapprox(best_actual, f_approx, atol = atol, rtol = rtol) - # convergence to obj -> tight cut - if isapprox(best_actual, obj, atol = atol, rtol = rtol) - lag_status = :aopt - # convergence to a smaller value than obj - # maybe possible due to numerical issues - # -> valid cut - else - lag_status = :conv - end - - # zero subgradients (and no further improvement), despite no convergence - # maybe possible due to numerical issues - # -> valid cut - elseif all(subgradients.== 0) - lag_status = :sub - - # lb exceeds ub: no convergence - elseif best_actual > f_approx + atol/10.0 + if L_star > t_k + atol/10.0 error("Could not solve for Lagrangian duals. LB > UB.") end + end - # return - if lag_status == :sub || lag_status == :aopt || lag_status == :conv - dual_vars .= best_mult - if dualsense == JuMP.MOI.MIN_SENSE - dual_vars .*= -1 - end - - for (i, (name, bin_state)) in enumerate(node.ext[:backward_data][:bin_states]) - #prepare_state_fixing!(node, state_comp) - JuMP.fix(bin_state, integrality_handler.old_rhs[i], force = true) - end - - if applied_solvers.MILP == "CPLEX" - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.MILP, "optcr"=>0.0, "numericalemphasis"=>0)) - elseif applied_solvers.MILP == "Gurobi" - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.MILP, "optcr"=>0.0, "NumericFocus"=>1)) - else - set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.MILP, "optcr"=>0.0)) - end - - return (lag_obj = best_actual, iterations = iter, lag_status = lag_status) - end - - # FORM A NEW LEVEL - ######################################################################## - if dualsense == :Min - level = f_approx + gap * level_factor - #TODO: + atol/10.0 for numerical issues? - JuMP.setupperbound(θ, level) + ############################################################################ + # CONVERGENCE ANALYSIS + ############################################################################ + if isapprox(L_star, t_k, atol = atol, rtol = rtol) + # CONVERGENCE ACHIEVED + if isapprox(L_star, s * primal_obj, atol = atol, rtol = rtol) + # CONVERGENCE TO TRUE OPTIMUM (APPROXIMATELY) + lag_status = :opt else - level = f_approx - gap * level_factor - #TODO: - atol/10.0 for numerical issues? - JuMP.setlowerbound(θ, level) - end + # CONVERGENCE TO A SMALLER VALUE THAN THE PRIMAL OBJECTIVE + # sometimes this occurs due to numerical issues + # still leads to a valid cut + lag_status = :conv + elseif all(h_k .== 0) + # NO OPTIMALITY ACHIEVED, BUT STILL ALL SUBGRADIENTS ARE ZERO + # may occur due to numerical issues + lag_status = :sub + elseif iter == iteration_limit + # TERMINATION DUE TO ITERATION LIMIT + # stil leads to a valid cut + lag_status = :iter + end - # DETERMINE NEXT ITERATE USING PROXIMAL PROBLEM - ######################################################################## - # Objective function of approx model has to be adapted to new center - JuMP.@objective(approx_model, Min, sum((dual_vars[i] - x[i])^2 for i=1:length(dual_vars))) - JuMP.optimize!(approx_model) - @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL + ############################################################################ + # APPLY MAGNANTI AND WONG APPROACH IF INTENDED + ############################################################################ + magnanti_wong(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, + iteration_limit, atol, rtol, algo_params.dual_choice_regime) - # Next iterate - dual_vars .= value.(x) - # can be deleted with the next update of GAMS.jl - replace!(dual_vars, NaN => 0) + ############################################################################ + # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) + ############################################################################ + restore_copy_constraints(node, x_in_value, algo_params.state_approximation_regime) - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #|| model.ext[:sddp_policy_graph].ext[:iteration] == 12 + ############################################################################ + # RESET SOLVER + ############################################################################ + set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) - # Logging - print_helper(print_lag_iteration, lag_log_file_handle, iter, f_approx, best_actual, f_actual) + ############################################################################ + # LOGGING + ############################################################################ + print_helper(print_lag_iteration, lag_log_file_handle, iter, t_k, L_star, L_k) - end + # Set dual_vars (here π_k) to the optimal solution + π_k = π_star - lag_status = :iter - #error("Could not solve for Lagrangian duals. Iteration limit exceeded.") - return (lag_obj = best_actual, iterations = iter, lag_status = lag_status) + return (lag_obj = s * L_star, iterations = iter, lag_status = lag_status) end diff --git a/stuff.jl b/stuff.jl index 2e4fbe36..37cc7625 100644 --- a/stuff.jl +++ b/stuff.jl @@ -35,6 +35,9 @@ typeof(return_results) + + + using JuMP using GLPK approx_model = JuMP.Model(GLPK.Optimizer) From c5ce01762d470086047479759a81c765efb101a2 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Tue, 12 Oct 2021 15:52:26 -0400 Subject: [PATCH 12/30] Added some comments to Level Bundle method and removed non-required parameters. --- src/lagrange.jl | 85 +++++++++++++++++++++++++++++++++++++++---------- src/typedefs.jl | 8 ++--- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/lagrange.jl b/src/lagrange.jl index a5fa88c1..374cc2a3 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -176,11 +176,6 @@ function solve_lagrangian_dual( L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - ######################################################################## - # ADD CUTTING PLANE - ######################################################################## - JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) - ######################################################################## # UPDATE BEST FOUND SOLUTION SO FAR ######################################################################## @@ -189,6 +184,11 @@ function solve_lagrangian_dual( π_star .= π_k end + ######################################################################## + # ADD CUTTING PLANE + ######################################################################## + JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) + ######################################################################## # SOLVE APPROXIMATION MODEL ######################################################################## @@ -472,8 +472,6 @@ function solve_lagrangian_dual( # Set bundle_parameters #--------------------------------------------------------------------------- level_factor = dual_solution_regime.level_factor - bundle_alpha = dual_solution.bundle_alpha - bundle_factor = dual_solution.bundle_factor ############################################################################ # RELAXING THE COPY CONSTRAINTS @@ -527,11 +525,6 @@ function solve_lagrangian_dual( L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - ######################################################################## - # ADD CUTTING PLANE - ######################################################################## - JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) - ######################################################################## # UPDATE BEST FOUND SOLUTION SO FAR ######################################################################## @@ -540,6 +533,11 @@ function solve_lagrangian_dual( π_star .= π_k end + ######################################################################## + # ADD CUTTING PLANE + ######################################################################## + JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) + ######################################################################## # RESET OBJECTIVE FOR APPROX_MODEL AFTER NONLINEAR MODEL ######################################################################## @@ -553,7 +551,6 @@ function solve_lagrangian_dual( JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL t_k = JuMP.objective_value(approx_model) - π_k .= JuMP.value(π) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #print("UB: ", f_approx, ", LB: ", f_actual) @@ -562,9 +559,64 @@ function solve_lagrangian_dual( ######################################################################## # COMPUTE GAP AND FORM A NEW LEVEL ######################################################################## - gap = abs(L_k - primal_obj) #t_k - level = t_k - gap * level_factor - #TODO: - atol/10.0 for numerical issues? + f_up = t_k # objective value of the approx_model + f_down = L_star # best lower bound so far + gap = f_up - f_down + + """ + We use a convex combination of f_up and f_down for the new level. + level = (1-a) f_up + a f_down = f_up - a (f_up - f_down) = f_up - a gap + + For level_factor = 0 => level = f_up, i.e. t_k, that means, this + reduces to the basic cutting-plane method. + + For level_factor = 1 => level = f_down, i.e. L_star. That means that + we search for the closest multiplier, for which the level is larger than + L_star. At least for L_star = L_k then the multiplier does not change + anymore, because the condition is trivially satisfied for the current + multiplier. Otherwise, this should yield one of the previous multipliers, + so again no improvement. + """ + + # TODO: POSSIBLE IMPROVEMENTS/CHANGES + """ + 1.) STRUCTURE + In the literature often a different order of the steps is proposed, + so maybe we should change this. + + In particular, the gap is often determined before the approx_model + is solved with the new multipliers. That means that the gap is determined + using f_down and f_up = t_{k-1}. In this case, t_0 has to be determined + appropriately, e.g. we can just choose primal_obj. + + 2.) STOPPING + In the literature it is often proposed to stop if the gap is sufficiently + small. In our case, this doesn't make a difference, but since the gap + can be determined differently as well, then our stopping criterion + would change. + + 3.) STABILITY CENTER + We always choose the new multiplier obtained by solving the proximal + problem as new stability center. It is also possible to change the + stability center only if a sufficiently large improvement is achieved. + + 4.) DETERMINING f_up + Instead of solving approx_model, we could also determine f_up in the + following way: + > If the proximal problem is infeasible, set f_up = level. + > If the proximal problem is feasible, do not change f_up. + + 5.) DETERMINE gap + Right now, the gap is determined as f_up - f_down with + f_up = t_k + f_down = L_star + + We could also choose f_up = primal_obj, since we already know that + this is the best possible upper bound. Is this beneficial? + """ + + level = f_up - gap * level_factor + # - atol/10.0 for numerical issues? JuMP.setlowerbound(t, level) ######################################################################## @@ -575,6 +627,7 @@ function solve_lagrangian_dual( set_solver(approx_model, algo_params, applied_solvers, :level_bundle) JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL + π_k .= JuMP.value(π) ######################################################################## if L_star > t_k + atol/10.0 diff --git a/src/typedefs.jl b/src/typedefs.jl index 44227e46..348e9d11 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -88,19 +88,15 @@ mutable struct LevelBundle <: AbstractDualSolutionRegime # atol::Float64 # rtol::Float64 # iteration_limit::Int - bundle_alpha::Float64 - bundle_factor::Float64 level_factor::Float64 function LevelBundle(; # atol = 1e-8, # rtol = 1e-8, # iteration_limit = 1000, - bundle_alpha = 1.0, - bundle_factor = 1.0, level_factor = 1.0, ) - #return new(atol, rtol, iteration_limit, bundle_alpha, bundle_factor, level_factor) - return new(bundle_alpha, bundle_factor, level_factor) + #return new(atol, rtol, iteration_limit, level_factor) + return new(level_factor) end end From 2db2f188648bff580d2592087ad8f23fd1e0c9a6 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:23:16 -0400 Subject: [PATCH 13/30] Added strengthened Benders cuts --- src/backwardPass.jl | 10 +-- src/backwardPass0.jl | 4 +- src/duals.jl | 161 +++++++++++++++++++++++++++++++++++++++---- src/forwardPass.jl | 8 +-- src/lagrange.jl | 56 ++++++++++++++- src/state.jl | 4 +- 6 files changed, 209 insertions(+), 34 deletions(-) diff --git a/src/backwardPass.jl b/src/backwardPass.jl index dd885f37..ca891421 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -227,7 +227,7 @@ function solve_subproblem_backward( # Parameterize the model. Fix the value of the incoming state variables. # Then parameterize the model depending on `noise` and set the objective. - set_incoming_state(node, state) + set_incoming_state!(node, state) parameterize(node, noise) @infiltrate algo_params.infiltrate_state in [:all] @@ -349,7 +349,7 @@ function solve_first_stage_problem( # Parameterize the model. First, fix the value of the incoming state # variables. Then parameterize the model depending on `noise`. Finally, # set the objective. - set_incoming_state(node, state) + set_incoming_state!(node, state) parameterize(node, noise) ############################################################################ @@ -357,12 +357,6 @@ function solve_first_stage_problem( ############################################################################ JuMP.optimize!(subproblem) - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - # Maybe attempt numerical recovery as in SDDP state = get_outgoing_state(node) diff --git a/src/backwardPass0.jl b/src/backwardPass0.jl index d44a83aa..0efd31a3 100644 --- a/src/backwardPass0.jl +++ b/src/backwardPass0.jl @@ -233,7 +233,7 @@ function solve_subproblem_backward( # Parameterize the model. Fix the value of the incoming state variables. # Then parameterize the model depending on `noise` and set the objective. - set_incoming_state(node, state) + set_incoming_state!(node, state) parameterize(node, noise) @infiltrate algo_params.infiltrate_state in [:all] @@ -520,7 +520,7 @@ function solve_first_stage_problem( # Parameterize the model. First, fix the value of the incoming state # variables. Then parameterize the model depending on `noise`. Finally, # set the objective. - set_incoming_state(node, state) + set_incoming_state!(node, state) parameterize(node, noise) ############################################################################ diff --git a/src/duals.jl b/src/duals.jl index dc533726..205732e0 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -12,6 +12,9 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ +#******************************************************************************* +# LP DUAL +#******************************************************************************* """ Solving the dual problem to obtain cut information - using LP relaxation @@ -25,8 +28,89 @@ function get_dual_solution( duality_regime::DynamicSDDiP.LinearDuality, ) + ############################################################################ + # SOME INITIALIZATIONS + ############################################################################ + subproblem = node.subproblem + + # storages for return of dual values and binary state values (trial point) + # note that with NoStateApproximation bin_state will just remain empty + dual_values = Dict{Symbol,Float64}() + bin_state = Dict{Symbol, BinaryState}() + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + + ############################################################################ + # INITIALIZE DUALS + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "LP_relaxation" begin + dual_results = solve_LP_relaxation(node, subproblem, algo_params, applied_solvers, duality_regime.DynamicSDDiP.LPDuals) + end + + dual_vars = dual_results.dual_vars + dual_obj = dual_results.dual_obj + + @infiltrate algo_params.infiltrate_state in [:all] + + ############################################################################ + # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN + ############################################################################ + store_dual_values!(node, dual_values, dual_vars, binary_state, integrality_handler, algo_params.state_approximation_regime) + + return ( + dual_values=dual_values, + bin_state=bin_state, + intercept=dual_obj, + iterations=1, + lag_status=nothing, + ) + + +function solve_LP_relaxation( + node::SDDP.Node, + subproblem::JuMP.Model, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + dual_initalization_regime::DynamicSDDiP.LPDuals, +) + + # Get number of states and create zero vector for duals + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + dual_vars_initial = zeros(number_of_states) + + # Create LP Relaxation + undo_relax = JuMP.relax_integrality(subproblem); + + # Define appropriate solver + set_solver(subproblem, algo_params, applied_solvers, :LP_relax) + + # Solve LP Relaxation + JuMP.optimize!(subproblem) + @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL + + dual_obj = JuMP.objective_value(subproblem) + + # Get dual values (reduced costs) for binary states as initial solution # TODO + get_and_set_dual_values!(node, dual_vars_initial, algo_params.state_approximation_regime) + + # Note: due to JuMP's dual convention, we need to flip the sign for + # maximization problems. + dual_sign = JuMP.objective_sense(node.subproblem) == MOI.MIN_SENSE ? 1 : -1 + dual_vars_initial = dual_sign * dual_vars_initial + + # Undo relaxation + undo_relax() + + return( + dual_obj = dual_obj + dual_vars = dual_vars_initial + ) end + +#******************************************************************************* +# STRENGTHENED DUAL +#******************************************************************************* + """ Solving the dual problem to obtain cut information - using LP relaxation and strengthening by Lagrangian relaxation @@ -40,8 +124,56 @@ function get_dual_solution( duality_regime::DynamicSDDiP.StrengthenedDuality, ) + ############################################################################ + # SOME INITIALIZATIONS + ############################################################################ + subproblem = node.subproblem + + # storages for return of dual values and binary state values (trial point) + # note that with NoStateApproximation bin_state will just remain empty + dual_values = Dict{Symbol,Float64}() + bin_state = Dict{Symbol, BinaryState}() + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + + ############################################################################ + # INITIALIZE DUALS + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "LP_relaxation" begin + dual_results = solve_LP_relaxation(node, subproblem, algo_params, applied_solvers, duality_regime.DynamicSDDiP.LPDuals) + end + + dual_vars = dual_results.dual_vars + dual_obj = dual_results.dual_obj + + @infiltrate algo_params.infiltrate_state in [:all] + + ############################################################################ + # GET STRENGTHENING INFORMATION + ############################################################################ + # solve lagrangian relaxed problem for these dual values + if node.has_integrality + dual_obj = _getStrengtheningInformation(node, dual_vars, algo_params, applied_solvers) + end + + ############################################################################ + # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN + ############################################################################ + store_dual_values!(node, dual_values, dual_vars, binary_state, integrality_handler, algo_params.state_approximation_regime) + + return ( + dual_values=dual_values, + bin_state=bin_state, + intercept=dual_obj, + iterations=1, + lag_status=nothing, + ) end + +#******************************************************************************* +# LAGRANGIAN DUAL +#******************************************************************************* + """ Solving the dual problem to obtain cut information - using Lagrangian dual """ @@ -93,13 +225,6 @@ function get_dual_solution( primal_obj = JuMP.objective_value(subproblem) @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL - # ADAPT TOTAL SOLVES APPROPRIATELY - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - # DEREGULARIZE PROBLEM IF REQUIRED deregularize_binary!(node, subproblem, algo_params.regularization_regime) @@ -151,7 +276,7 @@ function get_dual_solution( ############################################################################ # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN ############################################################################ - store_dual_values!(node, dual_vars, binary_state, integrality_handler, algo_params.state_approximation_regime) + store_dual_values!(node, dual_values, dual_vars, binary_state, integrality_handler, algo_params.state_approximation_regime) return ( dual_values=dual_values, @@ -162,6 +287,10 @@ function get_dual_solution( ) +#******************************************************************************* +# AUXILIARY METHODS +#******************************************************************************* + """ Determining objective and/or variable bounds for the Lagrangian dual if ValueBound is used. @@ -304,7 +433,7 @@ function initialize_duals( number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) dual_vars_initial = zeros(number_of_states) - return + return dual_vars_initial end @@ -332,14 +461,16 @@ function initialize_duals( # Solve LP Relaxation JuMP.optimize!(subproblem) + @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL + # or MOI.FEASIBLE_POINT??? - # Get dual values (reduced costs) for binary states as initial solution #TODO + # Get dual values (reduced costs) for binary states as initial solution # TODO get_and_set_dual_values!(node, dual_vars_initial, algo_params.state_approximation_regime) # Undo relaxation undo_relax() - return + return dual_vars_initial end @@ -359,14 +490,15 @@ function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Flo state_approximation_regime::DynamicSDDiP.NoState) for (i, name) in enumerate(keys(node,states)) - reference_to_constr = FixRef(name) + reference_to_constr = FixRef(name.in) dual_vars_initial[i] = JuMP.getdual(reference_to_constr) end return end -function store_dual_values!(node::SDDP.Node, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, +function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, + dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, integrality_handler::SDDP.SDDiP, state_approximation_regime::DynamicSDDiP.BinaryApproximation) for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) @@ -381,7 +513,8 @@ function store_dual_values!(node::SDDP.Node, dual_vars::Vector{Float64}, bin_sta return end -function store_dual_values!(node::SDDP.Node, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, +function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, + dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, integrality_handler::SDDP.SDDiP, state_approximation_regime::DynamicSDDiP.NoStateApproximation) for (i, name) in enumerate(keys(node.states)) diff --git a/src/forwardPass.jl b/src/forwardPass.jl index 75745c3b..03cf6c85 100644 --- a/src/forwardPass.jl +++ b/src/forwardPass.jl @@ -109,7 +109,7 @@ function solve_subproblem_forward( # Parameterize the model. First, fix the value of the incoming state # variables. Then parameterize the model depending on `noise`. Finally, # set the objective. - set_incoming_state(node, state) + set_incoming_state!(node, state) parameterize(node, noise) ############################################################################ @@ -126,12 +126,6 @@ function solve_subproblem_forward( @infiltrate infiltrate_state in [:all] JuMP.optimize!(subproblem) - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - # Maybe attempt numerical recovery as in SDDP state = get_outgoing_state(node) diff --git a/src/lagrange.jl b/src/lagrange.jl index 374cc2a3..cd04f10d 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -623,6 +623,7 @@ function solve_lagrangian_dual( # DETERMINE NEXT ITERATION USING PROXIMAL PROBLEM ######################################################################## # Objective function of approx model has to be adapted to new center + # TODO: Does this work with π[i]? JuMP.@objective(approx_model, Min, sum((π_k[i] - π[i])^2 for i in 1:number_of_states)) set_solver(approx_model, algo_params, applied_solvers, :level_bundle) JuMP.optimize!(approx_model) @@ -677,7 +678,7 @@ function solve_lagrangian_dual( ############################################################################ # LOGGING ############################################################################ - print_helper(print_lag_iteration, lag_log_file_handle, iter, t_k, L_star, L_k) + # print_helper(print_lag_iteration, lag_log_file_handle, iter, t_k, L_star, L_k) # Set dual_vars (here π_k) to the optimal solution π_k = π_star @@ -685,3 +686,56 @@ function solve_lagrangian_dual( return (lag_obj = s * L_star, iterations = iter, lag_status = lag_status) end + + +""" +Solve lagrangian relaxation to obtain intercept for strengthened Benders cuts +""" +function _getStrengtheningInformation( + node::SDDP.Node, + π_k::Vector{Float64}, + algo_params::NCNBD.AlgoParams, + applied_solvers::NCNBD.AppliedSolvers, + ) + + ############################################################################ + # INITIALIZATION + ############################################################################ + number_of_states = get_number_of_states(node, algo_params.state_approximation_regime) + # The original value for x (former old_rhs) + x_in_value = zeros(number_of_states) + # The current estimate for π (in our case determined in initialization) + # π_k + # The current value of ̄x-z (former subgradients) + h_k = zeros(number_of_states) + # The expression for ̄x-z (former slacks) + h_expr = Vector{AffExpr}(undef, number_of_states) + + # Set solver for inner problem + #--------------------------------------------------------------------------- + set_solver(node.subproblem, algo_params, applied_solvers, :lagrange_relax) + + ############################################################################ + # RELAXING THE COPY CONSTRAINTS + ############################################################################ + relax_copy_constraints(node, x_in_value, h_expr, algo_params.state_approximation_regime) + + ######################################################################## + # SOLVE LAGRANGIAN RELAXATION FOR GIVEN DUAL_VARS + ######################################################################## + # Evaluate the inner problem and determine a subgradient + Lag_obj = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, false) + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] + + ############################################################################ + # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) + ############################################################################ + restore_copy_constraints(node, x_in_value, algo_params.state_approximation_regime) + + ############################################################################ + # RESET SOLVER + ############################################################################ + set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + + return lag_obj +end diff --git a/src/state.jl b/src/state.jl index 52b07257..46b9413b 100644 --- a/src/state.jl +++ b/src/state.jl @@ -1,5 +1,5 @@ # The functions -# > "set_incoming_state", +# > "set_incoming_state!", # > "setup_state", # > "get_outgoing_state", # and structs @@ -37,7 +37,7 @@ end # Internal function: set the incoming state variables of node to the values # contained in state. -function set_incoming_state(node::SDDP.Node, state::Dict{Symbol,Float64}) +function set_incoming_state!!(node::SDDP.Node, state::Dict{Symbol,Float64}) for (state_name, value) in state # TODO: Check if required From a6fb40f14ea9efa2f56dc0604fd2ba0d540a5d29 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:33:26 -0400 Subject: [PATCH 14/30] Changed to consistent use of ! --- src/algorithmMain.jl | 23 ------------------- src/backwardPass.jl | 2 +- src/backwardPass0.jl | 2 +- src/binarization.jl | 4 ++-- src/duals.jl | 4 ++-- src/forwardPass.jl | 2 +- src/lagrange.jl | 53 +++++++++++++++++++++++-------------------- src/sigmaTest.jl | 2 +- src/solverHandling.jl | 2 +- 9 files changed, 38 insertions(+), 56 deletions(-) diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index 99f033c7..dd6dfda7 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -376,29 +376,6 @@ function convergence_handler(result::DynamicSDDiP.IterationResult, end -# The functions -# > "iteration", -# > "forward_pass", -# > "solve_subproblem_forward", -# > "solve_all_children", -# > "solve_subproblem_backward", -# > "get_dual_variables_backward", -# > "calculate_bound", -# > "solve_first_stage_problem" -# are derived from similar named functions (iteration, forward_pass, backward_pass, -# solve_all_children, solve_subproblem, get_dual_variables, calculate_bound, -# solve_first_stage_problem) in the 'SDDP.jl' package by -# Oscar Dowson and released under the Mozilla Public License 2.0. -# The reproduced function and other functions in this file are also released -# under Mozilla Public License 2.0 - -# Copyright (c) 2021 Christian Fuellner -# Copyright (c) 2021 Oscar Dowson - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - """ Executing an inner loop iteration for DynamicSDDiP. """ diff --git a/src/backwardPass.jl b/src/backwardPass.jl index ca891421..44cc40c0 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -242,7 +242,7 @@ function solve_subproblem_backward( ############################################################################ # RESET SOLVER (as it may have been changed in between for some reason) ############################################################################ - DynamicSDDiP.set_solver(subproblem, algo_params, applied_solvers, :backward_pass) + DynamicSDDiP.set_solver!(subproblem, algo_params, applied_solvers, :backward_pass) ############################################################################ # SOLVE DUAL PROBLEM TO OBTAIN CUT INFORMATION diff --git a/src/backwardPass0.jl b/src/backwardPass0.jl index 0efd31a3..30751091 100644 --- a/src/backwardPass0.jl +++ b/src/backwardPass0.jl @@ -254,7 +254,7 @@ function solve_subproblem_backward( # RESET SOLVER (as it may have been changed in between for some reason) ######################################################################## - DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :backward_pass) + DynamicSDDiP.set_solver!(node.subproblem, algo_params, applied_solvers, :backward_pass) # GET PRIMAL SOLUTION TO BOUND LAGRANGIAN DUAL ############################################################################ diff --git a/src/binarization.jl b/src/binarization.jl index c754dd7e..b101f4f0 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -54,7 +54,7 @@ function changeStateSpace!( epsilon = binary_precision[state_name] # Set up state for backward pass using binary approximation - setup_state_binarization(subproblem, state_comp, state_name, epsilon, bw_data) + setup_state_binarization!(subproblem, state_comp, state_name, epsilon, bw_data) end return @@ -149,7 +149,7 @@ end """ Setting up the binary state variables. """ -function setup_state_binarization( +function setup_state_binarization!( subproblem::JuMP.Model, state_comp::State, state_name::Symbol, diff --git a/src/duals.jl b/src/duals.jl index 205732e0..d398883d 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -81,7 +81,7 @@ function solve_LP_relaxation( undo_relax = JuMP.relax_integrality(subproblem); # Define appropriate solver - set_solver(subproblem, algo_params, applied_solvers, :LP_relax) + set_solver!(subproblem, algo_params, applied_solvers, :LP_relax) # Solve LP Relaxation JuMP.optimize!(subproblem) @@ -457,7 +457,7 @@ function initialize_duals( undo_relax = JuMP.relax_integrality(subproblem); # Define appropriate solver - set_solver(subproblem, algo_params, applied_solvers, :LP_relax) + set_solver!(subproblem, algo_params, applied_solvers, :LP_relax) # Solve LP Relaxation JuMP.optimize!(subproblem) diff --git a/src/forwardPass.jl b/src/forwardPass.jl index 03cf6c85..bafaee81 100644 --- a/src/forwardPass.jl +++ b/src/forwardPass.jl @@ -45,7 +45,7 @@ function forward_pass(model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, ######################################################################## # SET SOLVER ######################################################################## - DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + DynamicSDDiP.set_solver!(node.subproblem, algo_params, applied_solvers, :forward_pass) ######################################################################## # SUBPROBLEM SOLUTION diff --git a/src/lagrange.jl b/src/lagrange.jl index cd04f10d..3c968f76 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -121,7 +121,7 @@ function solve_lagrangian_dual( # Set solver for inner problem #--------------------------------------------------------------------------- - set_solver(node.subproblem, algo_params, applied_solvers, :lagrange_relax) + set_solver!(node.subproblem, algo_params, applied_solvers, :lagrange_relax) ############################################################################ # RELAXING THE COPY CONSTRAINTS @@ -140,13 +140,13 @@ function solve_lagrangian_dual( # Approximation of Lagrangian dual by cutting planes # Optimizer is re-set anyway approx_model = JuMP.Model(GLPK.Optimizer) - set_solver(approx_model, algo_params, applied_solvers, :kelley) + set_solver!(approx_model, algo_params, applied_solvers, :kelley) # Create the objective # Note that it is always formulated as a maximization problem, but that # s modifies the sense appropriately @variable(approx_model, t) - set_objective_bound(approx_model, s, bound_results.obj_bound) + set_objective_bound!(approx_model, s, bound_results.obj_bound) @objective(approx_model, Max, t) # Create the dual variables @@ -155,7 +155,7 @@ function solve_lagrangian_dual( @variable(approx_model, π⁺[1:number_of_states] >= 0) @variable(approx_model, π⁻[1:number_of_states] >= 0) @expression(approx_model, π, π⁺ .- π⁻) # not required to be a constraint - set_multiplier_bounds(approx_model, number_of_states bound_results.dual_bound) + set_multiplier_bounds!(approx_model, number_of_states bound_results.dual_bound) ############################################################################ # CUTTING-PLANE METHOD @@ -234,18 +234,18 @@ function solve_lagrangian_dual( ############################################################################ # APPLY MAGNANTI AND WONG APPROACH IF INTENDED ############################################################################ - magnanti_wong(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, + magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, iteration_limit, atol, rtol, algo_params.dual_choice_regime) ############################################################################ # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) ############################################################################ - restore_copy_constraints(node, x_in_value, algo_params.state_approximation_regime) + restore_copy_constraints!(node, x_in_value, algo_params.state_approximation_regime) ############################################################################ # RESET SOLVER ############################################################################ - set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + set_solver!(node.subproblem, algo_params, applied_solvers, :forward_pass) ############################################################################ # LOGGING @@ -312,7 +312,7 @@ function relax_copy_constraints!( return end -function restore_copy_constraints( +function restore_copy_constraints!( node::SDDP.Node, x_in_value::Vector{Float64} state_approximation_regime::DynamicSDDiP.BinaryApproximation, @@ -322,9 +322,11 @@ function restore_copy_constraints( #prepare_state_fixing!(node, state_comp) JuMP.fix(bin_state, x_in_value[i], force = true) end + + return end -function restore_copy_constraints( +function restore_copy_constraints!( node::SDDP.Node, x_in_value::Vector{Float64} state_approximation_regime::DynamicSDDiP.NoStateApproximation, @@ -334,15 +336,18 @@ function restore_copy_constraints( #prepare_state_fixing!(node, state_comp) JuMP.fix(state.in, x_in_value[i], force = true) end + + return end -function set_objective_bound(approx_model::JuMP.Model, s::Int, obj_bound::Float64) +function set_objective_bound!(approx_model::JuMP.Model, s::Int, obj_bound::Float64) JuMP.set_upper_bound(approx_model[:t], s * obj_bound) + return end -function set_multiplier_bounds(approx_model::JuMP.Model, number_of_states::Int, dual_bound::Float64) +function set_multiplier_bounds!(approx_model::JuMP.Model, number_of_states::Int, dual_bound::Float64) for i in 1:number_of_states JuMP.set_upper_bound(π⁺[i], dual_bound) @@ -358,7 +363,7 @@ Note that this is only done until the maximum number of iterations is achieved in total. """ -function magnanti_wong( +function magnanti_wong!( node::SDDP.Node, approx_model::JuMP.model, π_k::Vector{Float64}, @@ -401,7 +406,7 @@ function magnanti_wong( end -function magnanti_wong( +function magnanti_wong!( node::SDDP.Node, approx_model::JuMP.model, π_k::Vector{Float64}, @@ -467,7 +472,7 @@ function solve_lagrangian_dual( # Set solver for inner problem #--------------------------------------------------------------------------- - set_solver(node.subproblem, algo_params, applied_solvers, :lagrange_relax) + set_solver!(node.subproblem, algo_params, applied_solvers, :lagrange_relax) # Set bundle_parameters #--------------------------------------------------------------------------- @@ -495,7 +500,7 @@ function solve_lagrangian_dual( # Note that it is always formulated as a maximization problem, but that # s modifies the sense appropriately @variable(approx_model, t) - set_objective_bound(approx_model, s, bound_results.obj_bound) + set_objective_bound!(approx_model, s, bound_results.obj_bound) @objective(approx_model, Max, t) # Create the dual variables @@ -504,7 +509,7 @@ function solve_lagrangian_dual( @variable(approx_model, π⁺[1:number_of_states] >= 0) @variable(approx_model, π⁻[1:number_of_states] >= 0) @expression(approx_model, π, π⁺ .- π⁻) # not required to be a constraint - set_multiplier_bounds(approx_model, number_of_states bound_results.dual_bound) + set_multiplier_bounds!(approx_model, number_of_states bound_results.dual_bound) ############################################################################ # CUTTING-PLANE METHOD @@ -542,7 +547,7 @@ function solve_lagrangian_dual( # RESET OBJECTIVE FOR APPROX_MODEL AFTER NONLINEAR MODEL ######################################################################## JuMP.@objective(approx_model, Max, t) - set_solver(approx_model, algo_params, applied_solvers, :kelley) + set_solver!(approx_model, algo_params, applied_solvers, :kelley) ######################################################################## # SOLVE APPROXIMATION MODEL @@ -625,7 +630,7 @@ function solve_lagrangian_dual( # Objective function of approx model has to be adapted to new center # TODO: Does this work with π[i]? JuMP.@objective(approx_model, Min, sum((π_k[i] - π[i])^2 for i in 1:number_of_states)) - set_solver(approx_model, algo_params, applied_solvers, :level_bundle) + set_solver!(approx_model, algo_params, applied_solvers, :level_bundle) JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL π_k .= JuMP.value(π) @@ -662,18 +667,18 @@ function solve_lagrangian_dual( ############################################################################ # APPLY MAGNANTI AND WONG APPROACH IF INTENDED ############################################################################ - magnanti_wong(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, + magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, iteration_limit, atol, rtol, algo_params.dual_choice_regime) ############################################################################ # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) ############################################################################ - restore_copy_constraints(node, x_in_value, algo_params.state_approximation_regime) + restore_copy_constraints!(node, x_in_value, algo_params.state_approximation_regime) ############################################################################ # RESET SOLVER ############################################################################ - set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + set_solver!(node.subproblem, algo_params, applied_solvers, :forward_pass) ############################################################################ # LOGGING @@ -713,7 +718,7 @@ function _getStrengtheningInformation( # Set solver for inner problem #--------------------------------------------------------------------------- - set_solver(node.subproblem, algo_params, applied_solvers, :lagrange_relax) + set_solver!(node.subproblem, algo_params, applied_solvers, :lagrange_relax) ############################################################################ # RELAXING THE COPY CONSTRAINTS @@ -730,12 +735,12 @@ function _getStrengtheningInformation( ############################################################################ # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) ############################################################################ - restore_copy_constraints(node, x_in_value, algo_params.state_approximation_regime) + restore_copy_constraints!(node, x_in_value, algo_params.state_approximation_regime) ############################################################################ # RESET SOLVER ############################################################################ - set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + set_solver!(node.subproblem, algo_params, applied_solvers, :forward_pass) return lag_obj end diff --git a/src/sigmaTest.jl b/src/sigmaTest.jl index ad921c4c..1a835ea7 100644 --- a/src/sigmaTest.jl +++ b/src/sigmaTest.jl @@ -40,7 +40,7 @@ function forward_sigma_test( # SET SOLVER ######################################################################## - DynamicSDDiP.set_solver(node.subproblem, algo_params, applied_solvers, :forward_pass) + DynamicSDDiP.set_solver!(node.subproblem, algo_params, applied_solvers, :forward_pass) # SOLVE REGULARIZED PROBLEM ############################################################################ diff --git a/src/solverHandling.jl b/src/solverHandling.jl index 345eb538..15f3b18e 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -8,7 +8,7 @@ Function which assigns the correct solvers to be used in the specific part of DynamicSDDiP """ -function set_solver( +function set_solver!( subproblem::JuMP.Model, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, From 90884430fa369452b94d5b8086d6294edaaa78c1 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Wed, 13 Oct 2021 20:22:04 -0400 Subject: [PATCH 15/30] Updated Bellman implementation > includes update to new abstract data types and only MILP > includes different variants of cut projection --- src/algorithmMain.jl | 4 +- src/backwardPass.jl | 18 +- src/bellman.jl | 1333 +++++++++++++++++++++++++---------------- src/binarization.jl | 16 +- src/cutSelection.jl | 429 +++++++++++++ src/lagrange.jl | 12 +- src/solverHandling.jl | 9 +- src/typedefs.jl | 57 +- stuff.jl | 3 + 9 files changed, 1328 insertions(+), 553 deletions(-) create mode 100644 src/cutSelection.jl diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index dd6dfda7..d3b7c05e 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -82,7 +82,7 @@ function solve( #--------------------------------------------------------------------------- # fortunately, node.bellman_function requires no specific type for (key, node) in model.nodes - node.bellman_function = initialize_bellman_function_nonconvex(bellman_function, model, node) + node.bellman_function = DynamicSDDiP.initialize_bellman_function(bellman_function, model, node) node.bellman_function.cut_type = algo_params.cut_type node.bellman_function.global_theta.cut_oracle.deletion_minimum = algo_params.cut_selection_regime.cut_deletion_minimum @@ -91,6 +91,7 @@ function solve( end end + ############################################################################ # MODEL START ############################################################################ status = :not_solved @@ -107,6 +108,7 @@ function solve( finally end + ############################################################################ # lOG MODEL RESULTS ############################################################################ results = DynamicSDDiP.Results(status, log) diff --git a/src/backwardPass.jl b/src/backwardPass.jl index 44cc40c0..0de6f6f9 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -79,13 +79,21 @@ function backward_pass( applied_solvers ) + ######################################################################## # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS - #################################################################### + ######################################################################## anchor_states = determine_anchor_states(node, outgoing_state, algo_params.state_approximation_regime) @infiltrate algo_params.infiltrate_state in [:all] + ######################################################################## # REFINE BELLMAN FUNCTION BY ADDING CUTS - #################################################################### + ######################################################################## + """ + Note that for all backward openings, bin_state is the same, + so we can just use bin_state[1] in the following. + Maybe this should be changed later. + """ + TimerOutputs.@timeit DynamicSDDiP_TIMER "update_bellman" begin new_cuts = refine_bellman_function( model, @@ -95,7 +103,7 @@ function backward_pass( options.risk_measures[node_index], outgoing_state, anchor_states, - items.bin_state, + items.bin_state[1], items.duals, items.supports, items.probability, @@ -104,14 +112,16 @@ function backward_pass( applied_solvers ) end + push!(cuts[node_index], new_cuts) - #NOTE: Has to be adapted for stochastic case + # NOTE: This has to be adapted for stochastic case push!(model.ext[:lag_iterations], sum(items.lag_iterations)) push!(model.ext[:lag_status], items.lag_status[1]) #TODO: Implement cut-sharing as in SDDP end + return cuts end diff --git a/src/bellman.jl b/src/bellman.jl index ba36c06e..fd2cade0 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -1,17 +1,15 @@ # The functions # > "BellmanFunction", # > "bellman_term", -# > "initialize_bellman_function_nonconvex" +# > "initialize_bellman_function" # > "refine_bellman_function" # > "_add_average_cut" # > "_add_cut" # > "add_cut_constraints_to_models" -# > "_cut_selection_update" -# > "_eval_height" # and structs # > "SampledState", # > "LevelOneOracle", -# > "NonConvexApproximation" +# > "CutApproximation" # > "BellmanFunction" # are derived from similar named functions and structs in the 'SDDP.jl' package by # Oscar Dowson and released under the Mozilla Public License 2.0. @@ -25,48 +23,51 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ +################################################################################ +# DEFINING STRUCTURES TO STORE CUTS +################################################################################ mutable struct SampledState state::Dict{Symbol,Float64} - dominating_cut::DynamicSDDiP.NonlinearCut + dominating_cut::DynamicSDDiP.Cut best_objective::Float64 end mutable struct LevelOneOracle - cuts::Vector{DynamicSDDiP.NonlinearCut} + cuts::Vector{DynamicSDDiP.Cut} states::Vector{DynamicSDDiP.SampledState} - cuts_to_be_deleted::Vector{DynamicSDDiP.NonlinearCut} + cuts_to_be_deleted::Vector{DynamicSDDiP.Cut} deletion_minimum::Int function LevelOneOracle(deletion_minimum) - return new(DynamicSDDiP.NonlinearCut[], SDDP.SampledState[], DynamicSDDiP.NonlinearCut[], deletion_minimum) + return new(DynamicSDDiP.Cut[], SDDP.SampledState[], DynamicSDDiP.Cut[], deletion_minimum) end end -mutable struct NonConvexApproximation +mutable struct CutApproximation theta::JuMP.VariableRef states::Dict{Symbol,JuMP.VariableRef} - objective_states::Union{Nothing,NTuple{N,JuMP.VariableRef} where {N}} - belief_states::Union{Nothing,Dict{T,JuMP.VariableRef} where {T}} + # objective_states::Union{Nothing,NTuple{N,JuMP.VariableRef} where {N}} + # belief_states::Union{Nothing,Dict{T,JuMP.VariableRef} where {T}} cut_oracle::LevelOneOracle - function NonConvexApproximation( + function CutApproximation( theta::JuMP.VariableRef, states::Dict{Symbol,JuMP.VariableRef}, - objective_states, - belief_states, + # objective_states, + # belief_states, deletion_minimum::Int, ) return new( theta, states, - objective_states, - belief_states, + # objective_states, + # belief_states, LevelOneOracle(deletion_minimum), ) end end mutable struct BellmanFunction <: SDDP.AbstractBellmanFunction - global_theta::NonConvexApproximation - local_thetas::Vector{NonConvexApproximation} + global_theta::CutApproximation + local_thetas::Vector{CutApproximation} cut_type::SDDP.CutType # Cuts defining the dual representation of the risk measure. risk_set_cuts::Set{Vector{Float64}} @@ -79,7 +80,7 @@ function BellmanFunction(; lower_bound = -Inf, upper_bound = Inf, deletion_minimum::Int = 1, - cut_type::SDDP.CutType = SDDP.MULTI_CUT, + cut_type::SDDP.CutType = SDDP.SINGLE_CUT, ) return SDDP.InstanceFactory{BellmanFunction}( lower_bound = lower_bound, @@ -89,14 +90,18 @@ function BellmanFunction(; ) end -function bellman_term(bellman_function::NCNBD.BellmanFunction) +function bellman_term(bellman_function::DynamicSDDiP.BellmanFunction) return bellman_function.global_theta.theta end +################################################################################ +# INITIALIZE BELLMAN FUNCTION +################################################################################ + """ Initializing the bellman function for the subproblems. """ -function initialize_bellman_function_nonconvex( +function initialize_bellman_function( factory::SDDP.InstanceFactory{BellmanFunction}, model::SDDP.PolicyGraph{T}, node::SDDP.Node{T}, @@ -129,18 +134,22 @@ function initialize_bellman_function_nonconvex( upper_bound < Inf && JuMP.set_upper_bound(Θᴳ, upper_bound) # Initialize bounds for the objective states. If objective_state==nothing, # this check will be skipped by dispatch. - SDDP._add_initial_bounds(node.objective_state, Θᴳ) + # SDDP._add_initial_bounds(node.objective_state, Θᴳ) x′ = Dict(key => var.out for (key, var) in node.states) - obj_μ = node.objective_state !== nothing ? node.objective_state.μ : nothing - belief_μ = node.belief_state !== nothing ? node.belief_state.μ : nothing + ## obj_μ = node.objective_state !== nothing ? node.objective_state.μ : nothing + ## belief_μ = node.belief_state !== nothing ? node.belief_state.μ : nothing return BellmanFunction( - NonConvexApproximation(Θᴳ, x′, obj_μ, belief_μ, deletion_minimum), - NonConvexApproximation[], + ## CutApproximation(Θᴳ, x′, obj_μ, belief_μ, deletion_minimum), + CutApproximation(Θᴳ, x′, deletion_minimum), + CutApproximation[], cut_type, Set{Vector{Float64}}(), ) end +################################################################################ +# REFINE BELLMAN FUNCTION +################################################################################ """ Refining the bellman function of a node by constructing a new cut @@ -155,7 +164,7 @@ function refine_bellman_function( risk_measure::SDDP.AbstractRiskMeasure, trial_points::Dict{Symbol,Float64}, anchor_points::Dict{Symbol,Float64}, - bin_states::Vector{Dict{Symbol,BinaryState}}, + bin_states::Dict{Symbol,BinaryState}, dual_variables::Vector{Dict{Symbol,Float64}}, noise_supports::Vector, nominal_probability::Vector{Float64}, @@ -163,12 +172,19 @@ function refine_bellman_function( algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, ) where {T} - # Sanity checks. + + ############################################################################ + # CHECK IF ALL ELEMENTS CONTAINING INFORMATION ON ALL BACKWARD OPENINGS + # HAVE THE SAME/REQUIRED LENGTH + ############################################################################ @assert length(dual_variables) == length(noise_supports) == length(nominal_probability) == length(objective_realizations) == - length(bin_states) + + ############################################################################ + # RISK-RELATED PREPARATIOn + ############################################################################ # Preliminaries that are common to all cut types. risk_adjusted_probability = similar(nominal_probability) offset = SDDP.adjust_probability( @@ -180,7 +196,13 @@ function refine_bellman_function( model.objective_sense == MOI.MIN_SENSE, ) - # The meat of the function. + ############################################################################ + # ADD A NEW CUT TO THE CURRENT APPROXIMATION + ############################################################################ + """ Note that so far only single cuts are supported by DynamicSDDiP. + For multi cuts some more implementation is required. + """ + if bellman_function.cut_type == SDDP.SINGLE_CUT return _add_average_cut( node, @@ -198,16 +220,7 @@ function refine_bellman_function( ) else # Add a multi-cut @assert bellman_function.cut_type == SDDP.MULTI_CUT - #TODO: Not implemented so far - # SDDP._add_locals_if_necessary(node, bellman_function, length(dual_variables)) - # return _add_multi_cut( - # node, - # used_trial_points, - # risk_adjusted_probability, - # objective_realizations, - # dual_variables, - # offset, - # ) + #TODO: Not implemented so far, see SDDP.jl end end @@ -215,14 +228,12 @@ end """ Adding one more cut to the bellman function (taking expectations in stochastic case). """ -# Could also be shifted to SDDP.jl, since it overwrites an existing function, -# but with additional arguments. Therefore, both methods can be distinguished. function _add_average_cut( node::SDDP.Node, node_index::Int64, trial_points::Dict{Symbol,Float64}, anchor_points::Dict{Symbol,Float64}, - bin_states::Vector{Dict{Symbol,BinaryState}}, + bin_states::Dict{Symbol,BinaryState}, risk_adjusted_probability::Vector{Float64}, objective_realizations::Vector{Float64}, dual_variables::Vector{Dict{Symbol,Float64}}, @@ -232,13 +243,16 @@ function _add_average_cut( applied_solvers::DynamicSDDiP.AppliedSolvers, ) + # Some initializations N = length(risk_adjusted_probability) - @assert N == length(objective_realizations) == length(dual_variables) == length(bin_states) - bin_states = bin_states[1] + @assert N == length(objective_realizations) == length(dual_variables) + ############################################################################ + # EXPECTED INTERCEPT AND DUAL VARIABLES + ############################################################################ # Calculate the expected intercept and dual variables with respect to the # risk-adjusted probability distribution. - πᵏ = Dict(key => 0.0 for key in keys(bin_states)) + πᵏ = set_up_dict_for_duals(bin_states, trial_points, algo_params.state_approximation_regime) θᵏ = offset for i in 1:length(objective_realizations) @@ -249,86 +263,101 @@ function _add_average_cut( end end - # Now add the average-cut to the subproblem. We include the objective-state - # component μᵀy and the belief state (if it exists). - obj_y = node.objective_state === nothing ? nothing : node.objective_state.state - belief_y = node.belief_state === nothing ? nothing : node.belief_state.belief - + ############################################################################ + # GET CORRECT SIGMA + ############################################################################ # As cuts are created for the value function of the following state, - # we need the parameters for this stage - sigma = algo_params.sigma[node_index+1] + # we need the parameters for this stage. + sigma = algo_params.regularization_regime.sigma[node_index+1] + + ############################################################################ + # ADD THE CUT USING THE NEW EXPECTED COEFFICIENTS + ############################################################################ + # TODO: Hier dann Unterscheidung für state_approximation_regime _add_cut( node, node.bellman_function.global_theta, - node.ext[:lin_bellman_function].global_theta, θᵏ, πᵏ, bin_states, - trial_points, anchor_points, - obj_y, - belief_y, - algo_params.binary_precision, + trial_points, + # obj_y, + # belief_y, sigma, iteration, algo_params.infiltrate_state, + algo_params, applied_solvers, - cut_selection = algo_params.cut_selection + algo_params.state_approximation_regime, ) - return (theta = θᵏ, pi = πᵏ, λ = bin_states, obj_y = obj_y, belief_y = belief_y) + return (theta = θᵏ, pi = πᵏ, λ = bin_states) end """ -Adding one more cut based on dual information. +Adding one more cut based on dual information if BinaryApproximation is used. """ # Add the cut to the model and the convex approximation. function _add_cut( node::SDDP.Node, - V::NonConvexApproximation, - V_lin::NonConvexApproximation, - θᵏ::Float64, - πᵏ::Dict{Symbol,Float64}, - λᵏ::Dict{Symbol,BinaryState}, - trial_points::Dict{Symbol,Float64}, - xᵏ::Dict{Symbol,Float64}, - obj_y::Union{Nothing,NTuple{N,Float64}}, - belief_y::Union{Nothing,Dict{T,Float64}}, - binary_precision::Dict{Symbol,Float64}, + V::DynamicSDDiP.CutApproximation, + θᵏ::Float64, # epigraph variable theta + πᵏ::Dict{Symbol,Float64}, # dual multipliers (cut coefficients) + λᵏ::Dict{Symbol,BinaryState}, # binary states (anchor point in binary space for cut using BinaryApproximation) + xᵏ_b::Dict{Symbol,Float64}, # anchor point for cut using BinaryApproximation + xᵏ::Dict{Symbol,Float64}, # trial point (anchor point for cut without BinaryApproximation), outgoing_state + # obj_y::Union{Nothing,NTuple{N,Float64}}, + # belief_y::Union{Nothing,Dict{T,Float64}}, sigma::Float64, iteration::Int64, infiltrate_state::Symbol, - applied_solvers::DynamicSDDiP.AppliedSolvers; - cut_selection::Bool = false + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + state_approximation_regime::DynamicSDDiP.BinaryApproximation, ) where {N,T} - # CORRECT INTERCEPT + ############################################################################ + # CORRECT THE INTERCEPT (WE USE A DIFFERENT CUT FORMULA) ############################################################################ for (key, λ) in λᵏ θᵏ -= πᵏ[key] * λᵏ[key].value end @infiltrate infiltrate_state in [:bellman] + ############################################################################ # CONSTRUCT NONLINEAR CUT STRUCT ############################################################################ - #TODO: Should we add λᵏ? Actually, this information is not required. - cut = NonlinearCut(θᵏ, πᵏ, xᵏ, λᵏ, copy(binary_precision), copy(sigma), - JuMP.VariableRef[], JuMP.ConstraintRef[], - JuMP.VariableRef[], JuMP.ConstraintRef[], obj_y, belief_y, 1, iteration) + cut = DynamicSDDiP.NonlinearCut( + θᵏ, + πᵏ, + xᵏ, + xᵏ_b, + λᵏ, + copy(state_approximation_regime.binary_precision), + copy(sigma), + JuMP.VariableRef[], + JuMP.ConstraintRef[], + # obj_y, + # belief_y, + 1, + iteration + ) - # ADD CUT PROJECTION TO BOTH MODELS (MINLP AND MILP) ############################################################################ - add_cut_constraints_to_models(node, V, V_lin, cut, infiltrate_state) + # ADD CUT PROJECTION TO SUBPROBLEM (we are already at the previous stage) + ############################################################################ + add_cut_constraints_to_models(node, V, cut, algo_params, infiltrate_state) - if cut_selection - TimerOutputs.@timeit DynamicSDDiP_TIMER "cut_selection" begin - NCNBD._cut_selection_update(node, V, V_lin, cut, xᵏ, trial_points, applied_solvers, infiltrate_state) - end - else - push!(V.cut_oracle.cuts, cut) - push!(V_lin.cut_oracle.cuts, cut) + ############################################################################ + # UPDATE CUT SELECTION + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "cut_selection" begin + DynamicSDDiP._cut_selection_update(node, V, cut, xᵏ_b, xᵏ, + applied_solvers, algo_params, infiltrate_state, + algo_params.cut_selection_regime) end return @@ -336,179 +365,147 @@ end """ -Adding the constraints to described the new cut to the subproblems. +Adding the new cut to the subproblem in case of a NonlinearCut. """ function add_cut_constraints_to_models( node::SDDP.Node, - V::NonConvexApproximation, - V_lin::NonConvexApproximation, - cut::NonlinearCut, - #λᵏ::Dict{Symbol,Float64}, + V::DynamicSDDiP.CutApproximation, + cut::DynamicSDDiP.NonlinearCut, + algo_params::DynamicSDDiP.AlgoParams, infiltrate_state::Symbol, ) + ############################################################################ + # SOME INITIALIZATIONS + ############################################################################ model = JuMP.owner_model(V.theta) @assert model == node.subproblem - model_lin = JuMP.owner_model(V_lin.theta) - @assert model_lin == node.ext[:linSubproblem] - - # In gamma, all lambda (or here gamma) variables are stored, - # such that they can be multiplied with the cut_coefficients (the coefficients - # relate to the lambdas, but cannot be directly matched with the original states anymore - # as they are written into one vector) - # All other constraints and variables are introduced per state component - gamma = JuMP.VariableRef[] - gamma_lin = JuMP.VariableRef[] - allCoefficients = Float64[] - allCoefficients_lin = Float64[] - - duals_so_far = 0 - duals_lin_so_far = 0 - - # NOTE: Next steps are only done based on lin_states, - # since normal SDDP states do not have an info argument. - - # Determine maximum U for Big M constant - Umax = 0 - for (i, (name, state_comp)) in enumerate(node.ext[:lin_states]) - if state_comp.info.out.upper_bound > Umax - Umax = state_comp.info.out.upper_bound - end - end + number_of_states = length(node.states) - # ADD NEW VARIABLES AND CONSTRAINTS FOR CUT PROJECTION - ############################################################################ - for (i, (name, state_comp)) in enumerate(node.ext[:lin_states]) - if state_comp.info.out.binary + """ + Later on, in the cut formula, for each component of the dual vector + (in the binary space), we want to multiply the correct cut_coefficient + with a newly introduced (relaxed) binary variable. - # No cut projection required in binary state - # Store states in gamma though for general cut expression - push!(gamma, V.states[name]) - duals_so_far += 1 - - push!(gamma_lin, V_lin.states[name]) - duals_lin_so_far += 1 - - # Determine correct coefficient and add it to allCoefficients - # Maybe possible with where-statement - relatedCoefficient = 0.0 - for (i, (bin_name, value)) in enumerate(cut.coefficients) - if cut.binary_state[bin_name].x_name == name - relatedCoefficient = cut.coefficients[bin_name] - end - end - push!(allCoefficients, relatedCoefficient) - push!(allCoefficients_lin, relatedCoefficient) + We use the vector all_lambda to store all these [0,1] variables. + Additionally, we make sure that at the same index of the vector + all_coefficients the corresponding cut_coefficient is stored. - else - if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub - error("When using SDDiP, state variables require an upper bound.") - end - - if state_comp.info.out.integer - K = SDDP._bitsrequired(state_comp.info.out.upper_bound) - epsilon = 1 - else - epsilon = cut.binary_precision[name] - K = SDDP._bitsrequired(round(Int, state_comp.info.out.upper_bound / epsilon)) - end - - # Call function to add projection constraints and variables to - # model and model_Lin - duals_so_far = add_cut_projection_to_model!( - model, - V.states[name], - name, - cut.coefficients, - cut.binary_state, - gamma, - allCoefficients, - cut.iteration, - K, - epsilon, - cut.sigma, - cut.cutVariables, - cut.cutConstraints, - i, - duals_so_far, - Umax, - infiltrate_state) - duals_lin_so_far = add_cut_projection_to_model!( - model_lin, - V_lin.states[name], - name, - cut.coefficients, - cut.binary_state, - gamma_lin, - allCoefficients_lin, - cut.iteration, - K, - epsilon, - cut.sigma, - cut.cutVariables_lin, - cut.cutConstraints_lin, - i, - duals_lin_so_far, - Umax, - infiltrate_state) + In some cases to represent the cut projection closure, we need similar + vector storages all_eta and all_mu. - end - end + All other constraints and variables are introduced per state component. + + K_tilde counts how many components of the duals (in the binary space) + have been considered so far. + """ + all_coefficients = Float64[] + all_lambda = JuMP.VariableRef[] + all_eta = JuMP.VariableRef[] + all_mu = JuMP.VariableRef[] + K_tilde = 0 - # ADD ORIGINAL CUT DESCRIPTION ############################################################################ - yᵀμ = JuMP.AffExpr(0.0) - # if V.objective_states !== nothing - # for (y, μ) in zip(cut.obj_y, V.objective_states) - # JuMP.add_to_expression!(yᵀμ, y, μ) - # end - # end - # if V.belief_states !== nothing - # for (k, μ) in V.belief_states - # JuMP.add_to_expression!(yᵀμ, cut.belief_y[k], μ) - # end - # end + # ADD NEW VARIABLES AND CONSTRAINTS + ############################################################################ + for (state_index, (state_name, state_comp)) in enumerate(node.states) + """ + If a state is binary already, it may be possible to abstain from + introducing the cut projection / KKT constraints. That's actually + what I did in the NCNBD implementation. For example, it seems + unnecessary to introduce a new (relaxed) binary variable then. + However, I am not absolutely sure if we in fact do not require + the other constraints. Therefore, in this version, I also included + them for binary state variables. At least, if StrongDuality is used, + they are required anyway. + """ + + if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub || !state_comp.info.out.binary + error("When using DynamicSDDiP, state variables require an upper bound.") + end + + #################################################################### + # DETERMINE K (number of [0,1] variables required) AND BETA + #################################################################### + if state_comp.info.out.binary + beta = 1 + K = SDDP._bitsrequired(state_comp.info.out.upper_bound) + elseif state_comp.info.out.integer + beta = 1 + K = SDDP._bitsrequired(state_comp.info.out.upper_bound) + else + beta = cut.binary_precision[name] + K = SDDP._bitsrequired(round(Int, state_comp.info.out.upper_bound / beta)) + end + + #################################################################### + # DEFINE CUT PROJECTION CLOSURE + #################################################################### + # For the current state component add all required variables + # and constraints to represent the cut projection closure + K_tilde = represent_cut_projection_closure!( + model, + node, + ######################################################## + V.states[name], # state_comp + state_name, + state_index, + ######################################################## + cut.coefficients, + cut.binary_state, + cut.sigma, + cut.cut_variables, + cut.cut_constraints, + cut.iteration, + beta, + ######################################################## + all_coefficients, + all_lambda, + all_eta, + all_mu, + ######################################################## + K, + K_tilde, + ######################################################## + infiltrate_state, + ######################################################## + algo_params.state_approximation_regime.cut_projection_regime + ) + + end @infiltrate infiltrate_state in [:bellman] - @assert (size(gamma, 1) == size(collect(values(cut.coefficients)), 1) - == duals_so_far - == duals_lin_so_far - == size(gamma_lin, 1) - == size(allCoefficients, 1) - == size(allCoefficients_lin, 1) - ) - # == size(collect(values(λᵏ)), 1) - number_of_duals = size(gamma, 1) + ############################################################################ + # MAKE SOME VALIDITY CHECKS + ############################################################################ + validity_checks!(cut, V, K_tilde, all_lambda, all_mu, all_eta, + all_coefficients, number_of_states, + algo_params.state_approximation_regime.cut_projection_regime) + + number_of_duals = size(all_lambda, 1) - # TO ORIGINAL MODEL ############################################################################ - expr = @expression( - model, - V.theta + yᵀμ - sum(allCoefficients[j] * gamma[j] for j in 1:number_of_duals) - ) + # ADD THE ORIGINAL CUT CONSTRAINT AS WELL + ############################################################################ + expr = get_cut_expression(model, node, V, all_lambda, all_mu, all_eta, + all_coefficients, number_of_states, number_of_duals, + algo_params.state_approximation_regime.cut_projection_regime) constraint_ref = if JuMP.objective_sense(model) == MOI.MIN_SENSE @constraint(model, expr >= cut.intercept) else @constraint(model, expr <= cut.intercept) end - push!(cut.cutConstraints, constraint_ref) + push!(cut.cut_constraints, constraint_ref) - # TO LINEAR MODEL ############################################################################ - expr_lin = @expression( - model_lin, - V_lin.theta + yᵀμ - sum(allCoefficients[j] * gamma_lin[j] for j in 1:number_of_duals) - ) - - constraint_ref_lin = if JuMP.objective_sense(model_lin) == MOI.MIN_SENSE - @constraint(model_lin, expr_lin >= cut.intercept) - else - @constraint(model_lin, expr_lin <= cut.intercept) - end - push!(cut.cutConstraints_lin, constraint_ref_lin) + # ADD SOS1 STRONG DUALITY CONSTRAINT + ############################################################################ + add_strong_duality_cut(model, node, cut, V, all_lambda, all_mu, all_eta, + all_coefficients, number_of_states, number_of_duals, + algo_params.state_approximation_regime.cut_projection_regime) return @@ -516,385 +513,687 @@ end """ -Defining the constraints and variables corresponding to the cut projection -from binary to original space. +Defining the constraints and variables corresponding to representing +the cut projection closure. """ -function add_cut_projection_to_model!( +function represent_cut_projection_closure!( model::JuMP.Model, + node::SDDP.Node, + ######################################################## state_comp::JuMP.VariableRef, state_name::Symbol, + state_index::Int64, + ######################################################## coefficients::Dict{Symbol,Float64}, binary_state::Dict{Symbol,BinaryState}, - gamma::Vector{JuMP.VariableRef}, - allCoefficients::Vector{Float64}, + sigma::Float64, + cut_variables::Vector{JuMP.VariableRef}, + cut_constraints::Vector{JuMP.ConstraintRef}, iteration::Int64, + beta::Float64, + ######################################################## + all_coefficients::Vector{Float64}, + all_lambda::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + ######################################################## K::Int64, - epsilon::Float64, - sigma::Float64, - cutVariables::Vector{JuMP.VariableRef}, - cutConstraints::Vector{JuMP.ConstraintRef}, - i::Int64, - duals_so_far::Int64, - Umax::Float64, + K_tilde::Int64, + ######################################################## infiltrate_state::Symbol, + ######################################################## + cut_projection_regime::Union{DynamicSDDiP.SOS1, DynamicSDDiP.BigM, DynamicSDDiP.KKT} ) - # ADD VARIABLES FOR CUT PROJECTION - #################################################################### - ν = JuMP.@variable(model, [k in 1:K], lower_bound=0, base_name = "ν_" * string(i) * "_it" * string(iteration)) - μ = JuMP.@variable(model, [k in 1:K], lower_bound=0, base_name = "μ_" * string(i) * "_it" * string(iteration)) - η = JuMP.@variable(model, base_name = "η_" * string(i) * "_it" * string(iteration)) - γ = JuMP.@variable(model, [k in 1:K], lower_bound=0, upper_bound=1, base_name = "γ_" * string(i) * "_it" * string(iteration)) - w = JuMP.@variable(model, [k in 1:K], binary=true, base_name = "w_" * string(i) * "_it" * string(iteration)) - u = JuMP.@variable(model, [k in 1:K], binary=true, base_name = "u_" * string(i) * "_it" * string(iteration)) - append!(gamma, γ) - - #Store those variables! - append!(cutVariables, ν) - append!(cutVariables, μ) - push!(cutVariables, η) - #append!(cutVariables, γ) - append!(cutVariables, w) - append!(cutVariables, u) - - # ADD BINARY EXPANSION CONSTRAINT - #################################################################### - binary_constraint = JuMP.@constraint( - model, - state_comp == SDDP.bincontract([γ[k] for k in 1:K], epsilon) - ) - push!(cutConstraints, binary_constraint) - - # ADD KKT CONSTRAINT - #################################################################### - relatedCoefficients = Vector{Float64}(undef, K) + ############################################################################ + # STORE THE RELATED CUT COEFFICIENTS π + ############################################################################ + related_coefficients = Vector{Float64}(undef, K) for (i, (name, value)) in enumerate(coefficients) if binary_state[name].x_name == state_name index = binary_state[name].k - relatedCoefficients[index] = coefficients[name] + related_coefficients[index] = coefficients[name] end end - append!(allCoefficients, relatedCoefficients) + append!(all_coefficients, related_coefficients) + + ############################################################################ + # ADD REQUIRED VARIABLES (CPC-CONSTRAINTS 4, 5) + ############################################################################ + """ + Note that we use gamma instead of lambda here to avoid misconstructions + compared to the binary approximation in the backward pass. + """ + γ = JuMP.@variable(model, [k in 1:K], lower_bound=0, upper_bound=1, base_name = "γ_" * string(state_index) * "_it" * string(iteration)) + ν = JuMP.@variable(model, [k in 1:K], lower_bound=0, base_name = "ν_" * string(state_index) * "_it" * string(iteration)) + μ = JuMP.@variable(model, [k in 1:K], lower_bound=0, base_name = "μ_" * string(state_index) * "_it" * string(iteration)) + η = JuMP.@variable(model, base_name = "η_" * string(state_index) * "_it" * string(iteration)) + + # Store those variables in the storing vectors + append!(all_lambda, γ) + append!(all_mu, μ) + push!(all_eta, η) + + # Store those variables as cut_variables for the existing cut + append!(cut_variables, γ) + append!(cut_variables, ν) + append!(cut_variables, μ) + push!(cut_variables, η) - kkt_constraints = JuMP.@constraint( + ############################################################################ + # ADD BINARY EXPANSION CONSTRAINT (CPC-CONSTRAINT 3) + ############################################################################ + binary_constraint = JuMP.@constraint( + model, + state_comp == SDDP.bincontract([γ[k] for k in 1:K], beta) + ) + push!(cut_constraints, binary_constraint) + + ############################################################################ + # ADD DUAL FEASIBILITY CONSTRAINTS (CPC-CONSTRAINT 2) + ############################################################################ + primal_feas_constraints = JuMP.@constraint( model, [k=1:K], - -relatedCoefficients[k] - ν[k] + μ[k] + 2^(k-1) * epsilon * η == 0 + -related_coefficients[k] - ν[k] + μ[k] + 2^(k-1) * beta * η == 0 ) - append!(cutConstraints, kkt_constraints) + append!(cut_constraints, primal_feas_constraints) - # ADD BIG M CONSTRAINTS - #################################################################### - #bigM = 2 * sigma * Umax + ############################################################################ + # ADD COMPLEMENTARITY CONSTRAINTS + ############################################################################ + add_complementarity_constraints!( + model, + node, + ######################################################################## + state_index, + ######################################################################## + cut_variables, + cut_constraints, + sigma, + iteration, + beta, + ######################################################################## + related_coefficients + ######################################################################## + γ, + μ, + ν, + ######################################################################## + K, + ######################################################################## + infiltrate_state, + ######################################################################## + cut_projection_regime + ) - # new method for Big-M, since earlier method was probably not correct - bigM = 0 - for k in 1:K - candidate = Umax * (sigma + abs(relatedCoefficients[k]) / (2^(k-1) * epsilon)) - if bigM < candidate - bigM = candidate - end - end - # bigM = sigma + ############################################################################ + # INCREASE K_tilde + ############################################################################ + K_tilde += K + + return K_tilde + +end + +""" +Function representing complementarity constraints using projection_regime KKT. +""" +function add_complementarity_constraints!( + model::JuMP.Model, + node::SDDP.Node, + ######################################################################## + state_index::Int64, + ######################################################################## + cut_variables::Vector{JuMP.VariableRef}, + cut_constraints::Vector{JuMP.ConstraintRef}, + sigma::Float64, + iteration::Int64, + beta::Float64, + ######################################################################## + related_coefficients::Vector{Float64}, + ######################################################################## + γ::Vector{JuMP.VariableRef}, + μ::Vector{JuMP.VariableRef}, + ν::Vector{JuMP.VariableRef}, + ######################################################################## + K::Int64, + ######################################################################## + infiltrate_state::Symbol, + ######################################################################## + cut_projection_regime::DynamicSDDiP.KKT, +) @infiltrate infiltrate_state in [:bellman] + ############################################################################ + # ADD COMPLEMENTARITY CONSTRAINTS + ############################################################################ + complementarity_constraint_1 = JuMP.@constraint( + model, + [k=1:K], + ν[k] * γ[k] = 0 + ) + append!(cut_constraints, complementarity_constraint_1) + + complementarity_constraint_2 = JuMP.@constraint( + model, + [k=1:K], + μ[k] * (γ[k]-1) = 0 + ) + append!(cut_constraints, complementarity_constraint_2) + + return + +end + + +""" +Function representing complementarity constraints using projection_regime BigM. +""" +function add_complementarity_constraints!( + model::JuMP.Model, + node::SDDP.Node, + ######################################################################## + state_index::Int64, + ######################################################################## + cut_variables::Vector{JuMP.VariableRef}, + cut_constraints::Vector{JuMP.ConstraintRef}, + sigma::Float64, + iteration::Int64, + beta::Float64, + ######################################################################## + related_coefficients::Vector{Float64}, + ######################################################################## + γ::Vector{JuMP.VariableRef}, + μ::Vector{JuMP.VariableRef}, + ν::Vector{JuMP.VariableRef}, + ######################################################################## + K::Int64, + ######################################################################## + infiltrate_state::Symbol, + ######################################################################## + cut_projection_regime::DynamicSDDiP.BigM, +) + + @infiltrate infiltrate_state in [:bellman] + + ############################################################################ + # ADD ADDITIONAL BINARY VARIABLES + ############################################################################ + w = JuMP.@variable(model, [k in 1:K], binary=true, base_name = "w_" * string(state_index) * "_it" * string(iteration)) + u = JuMP.@variable(model, [k in 1:K], binary=true, base_name = "u_" * string(state_index) * "_it" * string(iteration)) + append!(cut_variables, w) + append!(cut_variables, u) + + ############################################################################ + # DETERMINE BIG-M PARAMETER + ############################################################################ + # TODO: Could be further improved later on + bigM = get_bigM(node, sigma, beta, related_coefficients, K) + + ############################################################################ + # ADD BIG-M CONSTRAINTS + ############################################################################ bigM_11_constraints = JuMP.@constraint( model, [k=1:K], γ[k] <= w[k] ) - append!(cutConstraints, bigM_11_constraints) + append!(cut_constraints, bigM_11_constraints) bigM_12_constraints = JuMP.@constraint( model, [k=1:K], ν[k] <= bigM * (1-w[k]) ) - append!(cutConstraints, bigM_12_constraints) + append!(cut_constraints, bigM_12_constraints) bigM_21_constraints = JuMP.@constraint( model, [k=1:K], 1 - γ[k] <= u[k] ) - append!(cutConstraints, bigM_21_constraints) + append!(cut_constraints, bigM_21_constraints) bigM_22_constraints = JuMP.@constraint( model, [k=1:K], μ[k] <= bigM * (1-u[k]) ) - append!(cutConstraints, bigM_22_constraints) + append!(cut_constraints, bigM_22_constraints) - duals_so_far += K - - return duals_so_far + return end """ -Simple cut selection feature. +Determine a reasonable bigM value based on the maximum upper bound of all state +components, sigma and beta. This could be improved later. """ -# Internal function: update the Level-One datastructures inside `bellman_function`. -function _cut_selection_update( - node::SDDP.Node, - V::NCNBD.NonConvexApproximation, - V_lin::NCNBD.NonConvexApproximation, - cut::NCNBD.NonlinearCut, - anchor_state::Dict{Symbol,Float64}, - trial_state::Dict{Symbol,Float64}, - applied_solvers::NCNBD.AppliedSolvers, - infiltrate_state::Symbol, -) - # if cut.obj_y !== nothing || cut.belief_y !== nothing - # # Skip cut selection if belief or objective states present. - # push!(V.cut_oracle.cuts, cut) - # return - # end +function get_bigM(node::SDDP.Node, sigma::Float64, beta::Float64, related_coefficients::Vector{Float64}, K::Int64) - # GET MODEL INFORMATION ############################################################################ - model = JuMP.owner_model(V.theta) - model_lin = JuMP.owner_model(V_lin.theta) - is_minimization = JuMP.objective_sense(model) == MOI.MIN_SENSE - oracle = V.cut_oracle - oracle_lin = V_lin.cut_oracle - - # GET TRIAL STATE AND BINARY STATE - ############################################################################ - sampled_state_anchor = NCNBD.SampledState(anchor_state, cut, _eval_height(node, cut, anchor_state, applied_solvers)) - sampled_state_trial = NCNBD.SampledState(trial_state, cut, _eval_height(node, cut, trial_state, applied_solvers)) - - # NOTE: By considering both type of states, we have way more states than - # cuts, so we may not eliminiate that many cuts. - # On the other hand, only considering the trial points is not sufficient, - # because it may lead to creating the same cuts over and over again if the - # binary approximation is not refined. - - # LOOP THROUGH PREVIOUSLY VISITED STATES (ANCHOR OR TRIAL STATES) - ############################################################################ - # Loop through previously sampled states and compare the height of the most recent cut - # against the current best. If this new cut is an improvement, store this one instead. - for old_state in oracle.states - height = _eval_height(node, cut, old_state.state, applied_solvers) - if SDDP._dominates(height, old_state.best_objective, is_minimization) - old_state.dominating_cut.non_dominated_count -= 1 - cut.non_dominated_count += 1 - old_state.dominating_cut = cut - old_state.best_objective = height - end - end - push!(oracle.states, sampled_state_anchor) - push!(oracle.states, sampled_state_trial) - push!(oracle_lin.states, sampled_state_anchor) - push!(oracle_lin.states, sampled_state_trial) - - # LOOP THROUGH PREVIOUSLY VISITED CUTS - ############################################################################ - # Now loop through previously discovered cuts and compare their height at - # `sampled_state`. If a cut is an improvement, add it to a queue to be added. - for old_cut in oracle.cuts - if !isempty(old_cut.cutConstraints) - # We only care about cuts not currently in the model. - continue - end + # DETERMINE U_MAX + ############################################################################ + U_max = 0 + for (i, (name, state)) in enumerate(node.states) - # For anchor state (is this required? the cuts should be tight here and - # we also do not have a stochastic program) - height = _eval_height(node, old_cut, anchor_state, applied_solvers) - if SDDP._dominates(height, sampled_state_anchor.best_objective, is_minimization) - sampled_state_anchor.dominating_cut.non_dominated_count -= 1 - old_cut.non_dominated_count += 1 - sampled_state_anchor.dominating_cut = old_cut - sampled_state_anchor.best_objective = height - add_cut_constraints_to_models(node, V, V_lin, old_cut, infiltrate_state) + if JuMP.upper_bound(state.in) > U_max + U_max = JuMP.upper_bound(state.in) end + end - # For trial state - height = _eval_height(node, old_cut, trial_state, applied_solvers) - if SDDP._dominates(height, sampled_state_trial.best_objective, is_minimization) - sampled_state_trial.dominating_cut.non_dominated_count -= 1 - old_cut.non_dominated_count += 1 - sampled_state_trial.dominating_cut = old_cut - sampled_state_trial.best_objective = height - add_cut_constraints_to_models(node, V, V_lin, old_cut, infiltrate_state) + ############################################################################ + # DETERMINE BIG-M + ############################################################################ + bigM = 0 + for k in 1:K + candidate = Umax * (sigma + abs(relatedCoefficients[k]) / (2^(k-1) * beta)) + if bigM < candidate + bigM = candidate end end - push!(oracle.cuts, cut) - push!(oracle_lin.cuts, cut) + # bigM = sigma + + return bigM +end + - # DETERMINE CUTS TO BE DELETED +""" +Function representing complementarity constraints using projection_regime KKT. +""" +function add_complementarity_constraints!( + model::JuMP.Model, + node::SDDP.Node, + ######################################################################## + state_index::Int64, + ######################################################################## + cut_variables::Vector{JuMP.VariableRef}, + cut_constraints::Vector{JuMP.ConstraintRef}, + sigma::Float64, + iteration::Int64, + beta::Float64, + ######################################################################## + related_coefficients::Vector{Float64}, + ######################################################################## + γ::Vector{JuMP.VariableRef}, + μ::Vector{JuMP.VariableRef}, + ν::Vector{JuMP.VariableRef}, + ######################################################################## + K::Int64, + ######################################################################## + infiltrate_state::Symbol, + ######################################################################## + cut_projection_regime::DynamicSDDiP.SOS1, +) + + @infiltrate infiltrate_state in [:bellman] + + ############################################################################ + # AUXILIARY VARIABLE ############################################################################ - #NOTE: The cuts to be deleted should be the same for V and V_lin, - #so in principle, it should not be required to determine this in two loops + # First represent γ[k]-1 as a new variable + ρ = JuMP.@variable(model, [k in 1:K], lower_bound=-1, upper__bound=0, base_name = "ρ_" * string(state_index) * "_it" * string(iteration)) + append!(cut_variables, ρ) - for cut in V.cut_oracle.cuts - if cut.non_dominated_count < 1 - if !isempty(cut.cutConstraints) - push!(oracle.cuts_to_be_deleted, cut) - end - end - end + ρ_constraint = JuMP.@constraint(model, [k in 1:K], ρ[k] == γ[k] - 1) + append!(cut_constraints, ρ_constraint) - for cut in V_lin.cut_oracle.cuts - if cut.non_dominated_count < 1 - if !isempty(cut.cutConstraints_lin) - push!(oracle_lin.cuts_to_be_deleted, cut) - end - end + ############################################################################ + # ADD SOS1 CONSTRAINTS + ############################################################################ + for k in 1:K + SOS1_constraint_1 = JuMP.@constraint(model, [γ[k], ν[k]] in SOS1()) + SOS1_constraint_2 = JuMP.@constraint(model, [μ[k], ρ[k]] in SOS1()) + append!(cut_constraints, SOS1_constraint_1) + append!(cut_constraints, SOS1_constraint_2) end - # DELETE CUTS FOR V AND V_LIN + return + +end + + +""" +Defining the constraints and variables corresponding to representing +the cut projection closure if StrongDuality is exploited. +""" +function represent_cut_projection_closure!( + model::JuMP.Model, + node::SDDP.Node, + ######################################################## + state_comp::JuMP.VariableRef, + state_name::Symbol, + state_index::Int64, + ######################################################## + coefficients::Dict{Symbol,Float64}, + binary_state::Dict{Symbol,BinaryState}, + sigma::Float64, + cut_variables::Vector{JuMP.VariableRef}, + cut_constraints::Vector{JuMP.ConstraintRef}, + iteration::Int64, + beta::Float64, + ######################################################## + all_coefficients::Vector{Float64}, + all_lambda::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + ######################################################## + K::Int64, + K_tilde::Int64, + ######################################################## + infiltrate_state::Symbol, + ######################################################## + cut_projection_regime::DynamicSDDiP.StrongDuality + ) + + ############################################################################ + # STORE THE RELATED CUT COEFFICIENTS π ############################################################################ - if length(oracle.cuts_to_be_deleted) >= oracle.deletion_minimum - for cut in oracle.cuts_to_be_deleted - for variable_ref in cut.cutVariables - JuMP.delete(model, variable_ref) - end - for constraint_ref in cut.cutConstraints - JuMP.delete(model, constraint_ref) - end - cut.cutVariables = JuMP.VariableRef[] - cut.cutConstraints = JuMP.ConstraintRef[] + related_coefficients = Vector{Float64}(undef, K) - #cut.non_dominated_count = 0 - end - end - empty!(oracle.cuts_to_be_deleted) - - if length(oracle_lin.cuts_to_be_deleted) >= oracle_lin.deletion_minimum - for cut in oracle_lin.cuts_to_be_deleted - for variable_ref in cut.cutVariables_lin - JuMP.delete(model_lin, variable_ref) - end - for constraint_ref in cut.cutConstraints_lin - JuMP.delete(model_lin, constraint_ref) - end - cut.cutVariables_lin = JuMP.VariableRef[] - cut.cutConstraints_lin = JuMP.ConstraintRef[] - - cut.non_dominated_count = 0 + for (i, (name, value)) in enumerate(coefficients) + if binary_state[name].x_name == state_name + index = binary_state[name].k + related_coefficients[index] = coefficients[name] end end - empty!(oracle_lin.cuts_to_be_deleted) - # DETERMINE NUMBER OF CUTS FOR LOGGING ############################################################################ - node.ext[:total_cuts] = size(V_lin.cut_oracle.cuts, 1) - counter = 0 - for cut in V_lin.cut_oracle.cuts - if !isempty(cut.cutConstraints_lin) - counter += 1 - end + # ADD REQUIRED VARIABLES (CPC-CONSTRAINTS 4, 5) + ############################################################################ + μ = JuMP.@variable(model, [k in 1:K], lower_bound=0, base_name = "μ_" * string(state_index) * "_it" * string(iteration)) + η = JuMP.@variable(model, base_name = "η_" * string(state_index) * "_it" * string(iteration)) + + # Store those variables in the storing vectors + append!(all_mu, μ) + push!(all_eta, η) + + # Store those variables as cut_variables for the existing cut + append!(cut_variables, μ) + push!(cut_variables, η) + + ############################################################################ + # ADD DUAL FEASIBILITY CONSTRAINTS (CPC-CONSTRAINT 2d) + ############################################################################ + primal_feas_constraints = JuMP.@constraint( + model, + [k=1:K], + μ[k] + 2^(k-1) * beta * η >= related_coefficients[k] + ) + append!(cut_constraints, primal_feas_constraints) + + ############################################################################ + # INCREASE K_tilde + ############################################################################ + K_tilde += K + + return K_tilde + +end + + +################################################################################ +# AUXILIARY FUNCTIONS +################################################################################ + +function set_up_dict_for_duals( + bin_states::Dict{Symbol,BinaryState}, + trial_points::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.BinaryApproximation + ) + + return Dict(key => 0.0 for key in keys(bin_states)) +end + +function set_up_dict_for_duals( + bin_states::Dict{Symbol,BinaryState}, + trial_points::Dict{Symbol,Float64}, + state_approximation_regime::DynamicSDDiP.BinaryApproximation + ) + + return Dict(key => 0.0 for key in keys(trial_points)) +end + +function validity_checks( + cut::DynamicSDDiP.NonlinearCut, + V::DynamicSDDiP.CutApproximation, + K_tilde::Int64, + all_lambda::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_coefficients::Vector{Float64}, + number_of_states::Int64, + cut_projection_regime::Union{DynamicSDDiP.SOS1,DynamicSDDiP.BigM,DynamicSDDiP.KKT}, + ) + + @assert (K_tilde == size(collect(values(cut.coefficients)), 1) + == size(all_coefficients, 1) + == size(all_mu, 1) + == size(all_lambda, 1) + ) + @assert (number_of_states == size(all_eta, 1) + == size(V.states, 1) + ) + + return +end + +function validity_checks( + cut::DynamicSDDiP.NonlinearCut, + V::DynamicSDDiP.CutApproximation, + K_tilde::Int64, + all_lambda::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_coefficients::Vector{Float64}, + number_of_states::Int64, + cut_projection_regime::DynamicSDDiP.StrongDuality, + ) + + @assert (K_tilde == size(collect(values(cut.coefficients)), 1) + == size(all_coefficients, 1) + == size(all_mu, 1) + ) + @assert (number_of_states == size(all_eta, 1) + == size(V.states, 1) + ) + + return +end + +function get_cut_expression( + model::JuMP.Model, + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + all_lambda::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_coefficients::Vector{Float64}, + number_of_states::Int64, + number_of_duals::Int64, + cut_projection_regime::Union{DynamicSDDiP.SOS1,DynamicSDDiP.BigM,DynamicSDDiP.KKT}, + ) + + expr = JuMP.@expression( + model, + V.theta - sum(all_coefficients[j] * all_lambda[j] for j in 1:number_of_duals) + ) + + return +end + +function get_cut_expression( + model::JuMP.Model, + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + all_lambda::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_coefficients::Vector{Float64}, + number_of_states::Int64, + number_of_duals::Int64, + cut_projection_regime::DynamicSDDiP.StrongDuality, + ) + + expr = JuMP.@expression( + model, + V.theta - sum(all_mu[j] for j in 1:number_of_duals) + - sum(x * all_eta[i] for (i, x) in V.states) + ) + + return +end + +function add_strong_duality_cut!( + model::JuMP.Model, + node::SDDP.Node, + cut::DynamicSDDiP.NonlinearCut, + V::DynamicSDDiP.CutApproximation, + all_lambda::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_coefficients::Vector{Float64}, + number_of_states::Int64, + number_of_duals::Int64, + cut_projection_regime::DynamicSDDiP.SOS1, + ) + + strong_duality_expr = JuMP.@expression( + model, + sum(all_coefficients[j] * all_lambda[j] for j in 1:number_of_duals) + - sum(all_mu[j] for j in 1:number_of_duals) + - sum(x * all_eta[i] for (i, x) in V.states) + ) + + constraint_ref = if JuMP.objective_sense(model) == MOI.MIN_SENSE + @constraint(model, strong_duality_expr >= 0) + else + @constraint(model, strong_duality_expr <= 0) end - node.ext[:active_cuts] = counter + push!(cut.cut_constraints, constraint_ref) return end +function add_strong_duality_cut!( + model::JuMP.Model, + node::SDDP.Node, + cut::DynamicSDDiP.NonlinearCut, + V::DynamicSDDiP.CutApproximation, + all_lambda::Vector{JuMP.VariableRef}, + all_mu::Vector{JuMP.VariableRef}, + all_eta::Vector{JuMP.VariableRef}, + all_coefficients::Vector{Float64}, + number_of_states::Int64, + number_of_duals::Int64, + cut_projection_regime::Union{DynamicSDDiP.BigM,DynamicSDDiP.KKT,DynamicSDDiP.StrongDuality}, + ) -""" -Evaluating the cuts at a specific point. -""" -# Internal function: calculate the height of `cut` evaluated at `state`. -function _eval_height(node::SDDP.Node, cut::NCNBD.NonlinearCut, states::Dict{Symbol,Float64}, applied_solvers::NCNBD.AppliedSolvers) + return +end - # Create a new JuMP model to evaluate the height of a non-convex cut - model = JuMP.Model() - JuMP.set_optimizer(model, optimizer_with_attributes(GAMS.Optimizer, "Solver"=>applied_solvers.LP, "optcr"=>0.0)) +################################################################################ - # Storages for coefficients and binary states - binary_state_storage = JuMP.VariableRef[] - allCoefficients = Float64[] - binary_variables_so_far = 0 +""" +Adding one more cut based on dual information if NoStateApproximation is used. +""" +# Add the cut to the model and the convex approximation. +function _add_cut( + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + θᵏ::Float64, # epigraph variable theta + πᵏ::Dict{Symbol,Float64}, # dual multipliers (cut coefficients) + λᵏ::Dict{Symbol,BinaryState}, # binary states (anchor point in binary space for cut using BinaryApproximation) + xᵏ_b::Dict{Symbol,Float64}, # anchor point for cut using BinaryApproximation + xᵏ::Dict{Symbol,Float64}, # trial point (anchor point for cut without BinaryApproximation), outgoing_state + # obj_y::Union{Nothing,NTuple{N,Float64}}, + # belief_y::Union{Nothing,Dict{T,Float64}}, + sigma::Float64, + iteration::Int64, + infiltrate_state::Symbol, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, + state_approximation_regime::DynamicSDDiP.NoStateApproximation, +) where {N,T} - for (i, (state_name, value)) in enumerate(states) - # Get actual state from state_name - state_comp = node.ext[:lin_states][state_name] + ############################################################################ + # CORRECT THE INTERCEPT (WE USE A DIFFERENT CUT FORMULA) + ############################################################################ + for (key, x) in xᵏ + θᵏ -= πᵏ[key] * x + end + @infiltrate infiltrate_state in [:bellman] - if state_comp.info.out.binary - # BINARY CASE - #################################################################### - # introduce one binary variable to the model - binary_var = JuMP.@variable(model) - push!(binary_state_storage, binary_var) - binary_variables_so_far += 1 - - # introduce binary expansion constraint to the model - binary_constraint = JuMP.@constraint(model, binary_var == value) - #TODO: Alternatively, we can just fix this variable - - # determine the correct cut coefficient - relatedCoefficient = 0.0 - for (bin_name, value) in cut.coefficients - if cut.binary_state[bin_name].x_name == state_name - relatedCoefficient = cut.coefficients[bin_name] - end - end - push!(allCoefficients, relatedCoefficient) + ############################################################################ + # CONSTRUCT NONLINEAR CUT STRUCT + ############################################################################ + cut = DynamicSDDiP.LinearCut( + θᵏ, + πᵏ, + xᵏ, + copy(sigma), + JuMP.ConstraintRef, + # obj_y, + # belief_y, + 1, + iteration + ) - else - if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub - error("When using SDDiP, state variables require an upper bound.") - end - - # INTEGER OR CONTINUOUS CASE - #################################################################### - # Get K and epsilon - if state_comp.info.out.integer - K = SDDP._bitsrequired(state_comp.info.out.upper_bound) - epsilon = 1 - else - epsilon = cut.binary_precision[state_name] - K = SDDP._bitsrequired(round(Int, state_comp.info.out.upper_bound / epsilon)) - end - - # introduce binary variables to the model - binary_var = JuMP.@variable(model, [k in 1:K], lower_bound=0, upper_bound=1) - append!(binary_state_storage, binary_var) - binary_variables_so_far += K - - # introduce binary expansion constraint to the model - binary_constraint = JuMP.@constraint(model, SDDP.bincontract([binary_var[k] for k in 1:K], epsilon) == value) - - # determine the correct cut coefficient - relatedCoefficients = Vector{Float64}(undef, K) - for (bin_name, value) in cut.coefficients - if cut.binary_state[bin_name].x_name == state_name - index = cut.binary_state[bin_name].k - relatedCoefficients[index] = cut.coefficients[bin_name] - end - end - append!(allCoefficients, relatedCoefficients) + ############################################################################ + # ADD CUT TO SUBPROBLEM (we are already at the previous stage) + ############################################################################ + add_cut_constraints_to_models(node, V, cut, algo_params, infiltrate_state) - end + ############################################################################ + # UPDATE CUT SELECTION + ############################################################################ + TimerOutputs.@timeit DynamicSDDiP_TIMER "cut_selection" begin + DynamicSDDiP._cut_selection_update(node, V, cut, xᵏ_b, xᵏ, + applied_solvers, algo_params, infiltrate_state, + algo_params.cut_selection_regime) end - @assert(size(allCoefficients, 1) == size(binary_state_storage, 1) - == binary_variables_so_far - == size(collect(values(cut.coefficients)),1) - ) + return +end - # ADD OBJECTIVE TO THE MODEL - #################################################################### - objective_sense_stage = JuMP.objective_sense(node.ext[:linSubproblem]) - eval_sense = ( - objective_sense_stage == JuMP.MOI.MIN_SENSE ? JuMP.MOI.MAX_SENSE : JuMP.MOI.MIN_SENSE + +""" +Adding the new cut to the subproblem in case of a LinearCut. +""" +function add_cut_constraints_to_models( + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + cut::DynamicSDDiP.LinearCut, + algo_params::DynamicSDDiP.AlgoParams, + infiltrate_state::Symbol, ) - JuMP.@objective( - model, eval_sense, - cut.intercept + sum(allCoefficients[j] * binary_state_storage[j] for j in 1:binary_variables_so_far) + ############################################################################ + # SOME INITIALIZATIONS + ############################################################################ + model = JuMP.owner_model(V.theta) + @assert model == node.subproblem + + @infiltrate infiltrate_state in [:bellman] + + ############################################################################ + # ADD THE LINEAR CUT CONSTRAINT + ############################################################################ + expr = @expression( + model, + V.theta - sum(cut.coefficients[i] * x for (i, x) in V.states) ) - # SOLVE MODEL AND RETURN SOLUTION - #################################################################### - JuMP.optimize!(model) - height = JuMP.objective_value(model) - return height + cut.constraint_ref = if JuMP.objective_sense(model) == MOI.MIN_SENSE + @constraint(model, expr >= cut.intercept) + else + @constraint(model, expr <= cut.intercept) + end + return end diff --git a/src/binarization.jl b/src/binarization.jl index b101f4f0..e13726e8 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -51,10 +51,10 @@ function changeStateSpace!( # Save fixed value of state fixed_value = JuMP.fix_value(state_comp.in) bw_data[:fixed_state_value][state_name] = fixed_value - epsilon = binary_precision[state_name] + beta = binary_precision[state_name] # Set up state for backward pass using binary approximation - setup_state_binarization!(subproblem, state_comp, state_name, epsilon, bw_data) + setup_state_binarization!(subproblem, state_comp, state_name, beta, bw_data) end return @@ -278,14 +278,14 @@ function setup_state_binarization!( #################################################################### # STATE VARIABLE IS CONTINUOUS #################################################################### - epsilon = binary_precision + beta = binary_precision """ see comment above for integer case """ #################################################################### # INTRODUCE BINARY VARIABLES TO THE PROBLEM #################################################################### - num_vars = SDDP._bitsrequired(round(Int, state_comp.info.in.upper_bound / epsilon)) + num_vars = SDDP._bitsrequired(round(Int, state_comp.info.in.upper_bound / beta)) binary_vars = JuMP.@variable( subproblem, @@ -309,7 +309,7 @@ function setup_state_binarization!( #################################################################### binary_constraint = JuMP.@constraint( subproblem, - state_comp.in == SDDP.bincontract([binary_vars[i] for i in 1:num_vars], epsilon) + state_comp.in == SDDP.bincontract([binary_vars[i] for i in 1:num_vars], beta) ) # store in list for later access and deletion @@ -319,7 +319,7 @@ function setup_state_binarization!( # FIX NEW VARIABLES #################################################################### # Get fixed values from fixed value of original state - fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], state_comp.info.in.upper_bound, epsilon) + fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], state_comp.info.in.upper_bound, beta) # Fix binary variables for i in 1:num_vars #JuMP.unset_binary(binary_vars[i].in) @@ -352,8 +352,8 @@ function determine_anchor_states( anchor_states = Dict{Symbol,Float64}() for (name, value) in outgoing_state state_comp = node.states[name] - epsilon = state_approximation_regime.binary_precision[name] - (approx_state_value, ) = determine_anchor_state(state_comp, value, epsilon) + beta = state_approximation_regime.binary_precision[name] + (approx_state_value, ) = determine_anchor_state(state_comp, value, beta) anchor_states[name] = approx_state_value end diff --git a/src/cutSelection.jl b/src/cutSelection.jl new file mode 100644 index 00000000..00dee3c9 --- /dev/null +++ b/src/cutSelection.jl @@ -0,0 +1,429 @@ +# The functions +# > "_cut_selection_update" +# > "_eval_height" +# are derived from equally named functions in the 'SDDP.jl' package by +# Oscar Dowson and released under the Mozilla Public License 2.0. +# The reproduced function and other functions in this file are also released +# under Mozilla Public License 2.0 + +# Copyright (c) 2021 Christian Fuellner +# Copyright (c) 2021 Oscar Dowson + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +""" +Trivial cut selection function if no cut selection is used. +""" +# Internal function: update the Level-One datastructures inside `bellman_function`. +function _cut_selection_update( + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + cut::Union{DynamicSDDiP.NonlinearCut,DynamicSDDiP.LinearCut} + anchor_state::Dict{Symbol,Float64}, + trial_state::Dict{Symbol,Float64}, + applied_solvers::DynamicSDDiP.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams, + infiltrate_state::Symbol, + cut_selection_regime::DynamicSDDiP.NoCutSelection +) + + return +end + + +""" +Simple cut selection feature for nonlinear cuts. +""" +# Internal function: update the Level-One datastructures inside `bellman_function`. +function _cut_selection_update( + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + cut::DynamicSDDiP.NonlinearCut + anchor_state::Dict{Symbol,Float64}, + trial_state::Dict{Symbol,Float64}, + applied_solvers::DynamicSDDiP.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams, + infiltrate_state::Symbol, + cut_selection_regime::DynamicSDDiP.CutSelection +) + + ############################################################################ + # GET MODEL INFORMATION + ############################################################################ + model = JuMP.owner_model(V.theta) + is_minimization = JuMP.objective_sense(model) == MOI.MIN_SENSE + oracle = V.cut_oracle + + ############################################################################ + # GET TRIAL STATE AND ANCHOR STATE + ############################################################################ + """ + By considering both type of states, we have way more states than cuts, + so we may not eliminiate that many cuts. On the other hand, + only considering the trial points is not sufficient, because it may lead to + creating the same cuts over and over again if the binary approximation + is not refined. Only considering the anchor points is also not sufficient, + since we are actually interested in good approximations in the trial points. + """ + sampled_state_anchor = DynamicSDDiP.SampledState(anchor_state, cut, _eval_height(node, cut, anchor_state, applied_solvers, algo_params)) + sampled_state_trial = DynamicSDDiP.SampledState(trial_state, cut, _eval_height(node, cut, trial_state, applied_solvers, algo_params)) + + ############################################################################ + # LOOP THROUGH PREVIOUSLY VISITED STATES (ANCHOR OR TRIAL STATES) + ############################################################################ + """ + We loop through previously sampled states and compare the height of the + most recent cut with the current best. + If the new cut yields an improvement, store this one instead. + """ + for old_state in oracle.states + height = _eval_height(node, cut, old_state.state, applied_solvers, algo_params) + if SDDP._dominates(height, old_state.best_objective, is_minimization) + old_state.dominating_cut.non_dominated_count -= 1 + cut.non_dominated_count += 1 + old_state.dominating_cut = cut + old_state.best_objective = height + end + end + push!(oracle.states, sampled_state_anchor) + push!(oracle.states, sampled_state_trial) + + ############################################################################ + # LOOP THROUGH PREVIOUSLY VISITED CUTS + ############################################################################ + """ + Now we loop through previously discovered cuts and compare their height + at the existing states. If a cut is an improvement, add it to a queue to be added. + """ + for old_cut in oracle.cuts + if !isempty(old_cut.cut_constraints) + # We only care about cuts not currently in the model. + continue + end + + # For anchor state (is this required? the cuts should be tight here and + # we also do not have a stochastic program) + height = _eval_height(node, old_cut, anchor_state, applied_solvers, algo_params) + if SDDP._dominates(height, sampled_state_anchor.best_objective, is_minimization) + sampled_state_anchor.dominating_cut.non_dominated_count -= 1 + old_cut.non_dominated_count += 1 + sampled_state_anchor.dominating_cut = old_cut + sampled_state_anchor.best_objective = height + add_cut_constraints_to_models(node, V, old_cut, algo_params, infiltrate_state) + end + + # For trial state + height = _eval_height(node, old_cut, trial_state, applied_solvers, algo_params) + if SDDP._dominates(height, sampled_state_trial.best_objective, is_minimization) + sampled_state_trial.dominating_cut.non_dominated_count -= 1 + old_cut.non_dominated_count += 1 + sampled_state_trial.dominating_cut = old_cut + sampled_state_trial.best_objective = height + add_cut_constraints_to_models(node, V, old_cut, algo_params, infiltrate_state) + end + end + push!(oracle.cuts, cut) + + ############################################################################ + # DETERMINE CUTS TO BE DELETED + ############################################################################ + for cut in V.cut_oracle.cuts + if cut.non_dominated_count < 1 + if !isempty(cut.cut_constraints) + push!(oracle.cuts_to_be_deleted, cut) + end + end + end + + ############################################################################ + # DELETE CUTS + ############################################################################ + if length(oracle.cuts_to_be_deleted) >= oracle.deletion_minimum + for cut in oracle.cuts_to_be_deleted + for variable_ref in cut.cut_variables + JuMP.delete(model, variable_ref) + end + for constraint_ref in cut.cut_constraints + JuMP.delete(model, constraint_ref) + end + cut.cut_variables = JuMP.VariableRef[] + cut.cut_constraints = JuMP.ConstraintRef[] + + cut.non_dominated_count = 0 + end + end + empty!(oracle.cuts_to_be_deleted) + + ############################################################################ + # DETERMINE NUMBER OF CUTS FOR LOGGING + ############################################################################ + counts_cuts(node, V) + + return +end + + +""" +Simple cut selection feature for linear cuts. +""" +# Internal function: update the Level-One datastructures inside `bellman_function`. +function _cut_selection_update( + node::SDDP.Node, + V::DynamicSDDiP.CutApproximation, + cut::DynamicSDDiP.LinearCut + anchor_state::Dict{Symbol,Float64}, + trial_state::Dict{Symbol,Float64}, + applied_solvers::DynamicSDDiP.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams, + infiltrate_state::Symbol, + cut_selection_regime::DynamicSDDiP.CutSelection +) + + ############################################################################ + # GET MODEL INFORMATION + ############################################################################ + model = JuMP.owner_model(V.theta) + is_minimization = JuMP.objective_sense(model) == MOI.MIN_SENSE + oracle = V.cut_oracle + + ############################################################################ + # GET TRIAL STATE + ############################################################################ + sampled_state = DynamicSDDiP.SampledState(trial_state, cut, _eval_height(node, cut, trial_state, applied_solvers, algo_params)) + + ############################################################################ + # LOOP THROUGH PREVIOUSLY VISITED STATES (ANCHOR OR TRIAL STATES) + ############################################################################ + """ + We loop through previously sampled states and compare the height of the + most recent cut with the current best. + If the new cut yields an improvement, store this one instead. + """ + for old_state in oracle.states + height = _eval_height(node, cut, old_state.state, applied_solvers, algo_params) + if SDDP._dominates(height, old_state.best_objective, is_minimization) + old_state.dominating_cut.non_dominated_count -= 1 + cut.non_dominated_count += 1 + old_state.dominating_cut = cut + old_state.best_objective = height + end + end + push!(oracle.states, sampled_state) + + ############################################################################ + # LOOP THROUGH PREVIOUSLY VISITED CUTS + ############################################################################ + """ + Now we loop through previously discovered cuts and compare their height + at the existing states. If a cut is an improvement, add it to a queue to be added. + """ + for old_cut in oracle.cuts + if !isempty(old_cut.cut_constraints) + # We only care about cuts not currently in the model. + continue + end + + # For trial state + height = _eval_height(node, old_cut, trial_state, applied_solvers, algo_params) + if SDDP._dominates(height, sampled_state_trial.best_objective, is_minimization) + sampled_state_trial.dominating_cut.non_dominated_count -= 1 + old_cut.non_dominated_count += 1 + sampled_state_trial.dominating_cut = old_cut + sampled_state_trial.best_objective = height + add_cut_constraints_to_models(node, V, old_cut, algo_params, infiltrate_state) + end + end + push!(oracle.cuts, cut) + + ############################################################################ + # DETERMINE CUTS TO BE DELETED + ############################################################################ + for cut in V.cut_oracle.cuts + if cut.non_dominated_count < 1 + if cut.cut_constraint !== nothing + push!(oracle.cuts_to_be_deleted, cut) + end + end + end + + ############################################################################ + # DELETE CUTS + ############################################################################ + if length(oracle.cuts_to_be_deleted) >= oracle.deletion_minimum + for cut in oracle.cuts_to_be_deleted + JuMP.delete(model, cut.cut_constraint) + cut.cut_constraint = nothing + cut.non_dominated_count = 0 + end + end + empty!(oracle.cuts_to_be_deleted) + + ############################################################################ + # DETERMINE NUMBER OF CUTS FOR LOGGING + ############################################################################ + counts_cuts(node, V) + +end + + +function count_cuts(node::SDDP.Node, V::DynamicSDDiP.CutApproximation) + node.ext[:total_cuts] = size(V.cut_oracle.cuts, 1) + counter = 0 + + for cut in V.cut_oracle.cuts + counter = count_cut(cut, counter) + end + node.ext[:active_cuts] = counter + + return + +end + +function count_cut(cut::DynamicSDDiP.NonlinearCut, counter::Int64) + + if !isempty(cut.cut_constraints) + counter += 1 + end + + return counter +end + +function count_cut(cut::DynamicSDDiP.LinearCut, counter::Int64) + + if cut.cut_constraint !== nothing + counter += 1 + end + + return counter +end + + +""" +Evaluating some nonlinear cut at a specific point. +""" +# Internal function: calculate the height of `cut` evaluated at `state`. +function _eval_height(node::SDDP.Node, cut::DynamicSDDiP.NonlinearCut, + states::Dict{Symbol,Float64}, applied_solvers::DynamicSDDiP.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams) + + ############################################################################ + # CREATE CUT-PROJECTION CLOSURE LP TO EVALUATE CUT + ############################################################################ + # Create a new JuMP model to evaluate the height of a non-convex cut + model = JuMP.Model() + set_solver!(model, algo_params, applied_solvers, :cut_selection) + + # Storages for coefficients and binary states + binary_state_storage = JuMP.VariableRef[] + all_coefficients = Float64[] + binary_variables_so_far = 0 + + for (i, (state_name, state_comp)) in enumerate(node.states) + if state_comp.info.out.binary + #################################################################### + # BINARY CASE + #################################################################### + # introduce one binary variable to the model + binary_var = JuMP.@variable(model) + push!(binary_state_storage, binary_var) + binary_variables_so_far += 1 + + # introduce binary expansion constraint to the model + binary_constraint = JuMP.@constraint(model, binary_var == value) + + # determine the correct cut coefficient + related_coefficient = 0.0 + for (bin_name, value) in cut.coefficients + if cut.binary_state[bin_name].x_name == state_name + related_coefficient = cut.coefficients[bin_name] + end + end + push!(all_coefficients, related_cefficient) + + else + #################################################################### + # NON-BINARY CASE + #################################################################### + if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub + error("When using SDDiP, state variables require an upper bound.") + end + + # Get K and beta + if state_comp.info.out.integer + beta = 1 + K = SDDP._bitsrequired(state_comp.info.out.upper_bound) + else + beta = cut.binary_precision[state_name] + K = SDDP._bitsrequired(round(Int, state_comp.info.out.upper_bound / beta)) + end + + # introduce binary variables to the model + binary_var = JuMP.@variable(model, [k in 1:K], lower_bound=0, upper_bound=1) + append!(binary_state_storage, binary_var) + binary_variables_so_far += K + + # introduce binary expansion constraint to the model + binary_constraint = JuMP.@constraint(model, SDDP.bincontract([binary_var[k] for k in 1:K], beta) == value) + + # determine the correct cut coefficient + related_coefficients = Vector{Float64}(undef, K) + for (bin_name, value) in cut.coefficients + if cut.binary_state[bin_name].x_name == state_name + index = cut.binary_state[bin_name].k + related_coefficients[index] = cut.coefficients[bin_name] + end + end + append!(all_coefficients, related_coefficients) + + end + end + + ############################################################################ + # SOME VALIDITY CHECKS + ############################################################################ + @assert(size(all_coefficients, 1) == size(binary_state_storage, 1) + == binary_variables_so_far + == size(collect(values(cut.coefficients)),1) + ) + + ############################################################################ + # ADD OBJECTIVE TO THE MODEL + ############################################################################ + objective_sense_stage = JuMP.objective_sense(node.subproblem) + eval_sense = ( + objective_sense_stage == JuMP.MOI.MIN_SENSE ? JuMP.MOI.MAX_SENSE : JuMP.MOI.MIN_SENSE + ) + + JuMP.@objective( + model, eval_sense, + cut.intercept + sum(all_coefficients[j] * binary_state_storage[j] for j in 1:binary_variables_so_far) + ) + + ############################################################################ + # SOLVE MODEL AND RETURN SOLUTION + ############################################################################ + JuMP.optimize!(model) + height = JuMP.objective_value(model) + return height + +end + + +""" +Evaluating some linear cut at a specific point. +""" +# Internal function: calculate the height of `cut` evaluated at `state`. +function _eval_height(node::SDDP.Node, cut::DynamicSDDiP.LinearCut, + states::Dict{Symbol,Float64}, applied_solvers::DynamicSDDiP.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams) + + # Start with intercept and increase by evaluating the states + height = cut.intercept + for (key, value) in cut.coefficients + height += value * states[key] + end + return height + +end diff --git a/src/lagrange.jl b/src/lagrange.jl index 3c968f76..cedc1fbb 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -86,8 +86,8 @@ function solve_lagrangian_dual( primal_obj::Float64, π_k::Vector{Float64}, bound_results::Tuple{Float64,Float64} - algo_params::NCNBD.AlgoParams, - applied_solvers::NCNBD.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.Kelley ) @@ -437,8 +437,8 @@ function solve_lagrangian_dual( primal_obj::Float64, π_k::Vector{Float64}, bound_results::Tuple{Float64,Float64} - algo_params::NCNBD.AlgoParams, - applied_solvers::NCNBD.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.LevelBundle ) @@ -699,8 +699,8 @@ Solve lagrangian relaxation to obtain intercept for strengthened Benders cuts function _getStrengtheningInformation( node::SDDP.Node, π_k::Vector{Float64}, - algo_params::NCNBD.AlgoParams, - applied_solvers::NCNBD.AppliedSolvers, + algo_params::DynamicSDDiP.AlgoParams, + applied_solvers::DynamicSDDiP.AppliedSolvers, ) ############################################################################ diff --git a/src/solverHandling.jl b/src/solverHandling.jl index 15f3b18e..da41d666 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -15,23 +15,26 @@ function set_solver!( algorithmic_step::Symbol, ) + cut_projection_regime = algo_params.state_approximation_regime.cut_projection_regime + # CHOOSE THE CORRECT TYPE OF SOLVER AND SOLVER ############################################################################ if algorithmic_step in [:forward_pass, :backward_pass] - if algo_params.cut_projection_method == :Bilinear + if cut_projection_regime == DynamicSDDiP.KKT || cut_projection_regime == DynamicSDDiP.StrongDuality solver = applied_solvers.MINLP else solver = applied_solvers.MILP end elseif algorithmic_step in [:lagrange_relax] - if algo_params.cut_projection_method == :Bilinear + if cut_projection_regime == DynamicSDDiP.KKT || cut_projection_regime == DynamicSDDiP.StrongDuality solver = applied_solvers.MINLP else solver = applied_solvers.lagrange end elseif algorithmic_step in [:level_bundle] solver = applied_solvers.NLP - elseif algorithmic_step in [:LP_relax, :kelley] + elseif algorithmic_step in [:LP_relax, :kelley, :cut_selection] + # TODO: What about nonlinear cut projections here? solver = applied_solvers.LP end diff --git a/src/typedefs.jl b/src/typedefs.jl index 348e9d11..e7dd8cfc 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -168,15 +168,23 @@ BigM means that the complementarity constraints of the KKT conditions of the cut projection closure are reformulated using a big-M approach. SOS1 means that the complemnentarity constraints of the KKT conditions of the cut projection closure are reformulated using SOS1 constraints. -Bilinear means that by using strong duality the cuts are integrated in a - bilinear way. This implies that MINLP solvers have to be used to solve - the subproblems. +KKT means that the complementarity constraints of the KKT conditions are used + in their original bilinear form. +StrongDuality means that by using strong duality the cuts are integrated in a + bilinear way. Default is BigM. + +Note that for KKT and StrongDuality the subproblems become nonlinear, and thus + an MINLP solver (e.g. Gurobi) has to be used to solve them. + Moreover, in these cases, only zeros should be used as initialization + method and only Lagrangian cuts should be determined, since the LP + relaxation is no LP anymore and may yield useless results. """ mutable struct BigM <: AbstractCutProjectionRegime end mutable struct SOS1 <: AbstractCutProjectionRegime end -mutable struct Bilinear <: AbstractCutProjectionRegime end +mutable struct KKT <: AbstractCutProjectionRegime end +mutable struct StrongDuality <: AbstractCutProjectionRegime end ################################################################################ # BINARY APPROXIMATION @@ -393,8 +401,33 @@ struct AppliedSolvers end ################################################################################ -# DEFINING NONLINEAR CUTS +# DEFINING CUTS ################################################################################ + +abstract type Cut end + +mutable struct NonlinearCut <: Cut + intercept::Float64 + coefficients::Dict{Symbol,Float64} + ############################################################################ + trial_state::Dict{Symbol,Float64} + anchor_state::Dict{Symbol,Float64} + binary_state::Dict{Symbol,BinaryState} + binary_precision::Dict{Symbol,Float64} + ############################################################################ + sigma::Float64 + ############################################################################ + cut_variables::Vector{JuMP.VariableRef} + cut_constraints::Vector{JuMP.ConstraintRef} + ############################################################################ + # obj_y::Union{Nothing,NTuple{N,Float64} where {N}} #TODO + # belief_y::Union{Nothing,Dict{T,Float64} where {T}} #TODO + ############################################################################ + non_dominated_count::Int + ############################################################################ + iteration::Int64 +end + """ The first two arguments define the cut coefficients (in the binary space). @@ -421,22 +454,18 @@ Note that if no binary approximation is used, trial_state and anchor_state will always be the same and only one cut_constraint has to be stored. """ -mutable struct NonlinearCut +mutable struct LinearCut <: Cut intercept::Float64 coefficients::Dict{Symbol,Float64} ############################################################################ - trial_state::Dict{Symbol,Float64} - anchor_state::Dict{Symbol,Float64} - binary_state::Dict{Symbol,BinaryState} - binary_precision::Dict{Symbol,Float64} + trial_state::Dict{Symbol,Float64} # same as anchor state ############################################################################ sigma::Float64 ############################################################################ - cut_variables::Vector{JuMP.VariableRef} - cut_constraints::Vector{JuMP.ConstraintRef} + cut_constraint::JuMP.ConstraintRef ############################################################################ - obj_y::Union{Nothing,NTuple{N,Float64} where {N}} #TODO - belief_y::Union{Nothing,Dict{T,Float64} where {T}} #TODO + # obj_y::Union{Nothing,NTuple{N,Float64} where {N}} + # belief_y::Union{Nothing,Dict{T,Float64} where {T}} ############################################################################ non_dominated_count::Int ############################################################################ diff --git a/stuff.jl b/stuff.jl index 37cc7625..78377458 100644 --- a/stuff.jl +++ b/stuff.jl @@ -41,6 +41,9 @@ typeof(return_results) using JuMP using GLPK approx_model = JuMP.Model(GLPK.Optimizer) +ν = JuMP.@variable(approx_model, [1:10], lower_bound=0) +typeof(ν) + @variable(approx_model, t <= 100) @objective(approx_model, Max, t) From 2b8e330bc1d1fcaa83ab90c2f1b447d2c71b1ed0 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Wed, 13 Oct 2021 23:00:15 -0400 Subject: [PATCH 16/30] Logging of StoppingRules --- src/logging.jl | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/logging.jl b/src/logging.jl index 4fe55f10..c432f728 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -107,12 +107,23 @@ function print_parameters(io, algo_params::DynamicSDDiP.AlgoParams, applied_solv println(io) println(io) - # Printing the parameters used - # TODO: Stopping rules - println(io, Printf.@sprintf("opt_rtol: %1.4e", algo_params.opt_rtol)) - println(io, Printf.@sprintf("opt_atol: %1.4e", algo_params.opt_atol)) - println(io, Printf.@sprintf("iteration_limit: %5d", algo_params.iteration_limit)) - println(io, Printf.@sprintf("time_limit (sec): %6d", algo_params.time_limit)) + ############################################################################ + # PRINTING THE PARAMETERS USED + ############################################################################ + if isempty(algo_params.stopping_rules) + println(io, "No stopping rule defined.") + else + for i in algo_params.stopping_rules + if isa(algo_params.stopping_rules[i], DeterministicStopping) + println(io, Printf.@sprintf("opt_rtol: %1.4e", algo_params.opt_rtol)) + println(io, Printf.@sprintf("opt_atol: %1.4e", algo_params.opt_atol)) + elseif isa(algo_params.stopping_rules[i], SDDP.IterationLimit) + println(io, Printf.@sprintf("iteration_limit: %5d", algo_params.iteration_limit)) + elseif isa(algo_params.stopping_rules[i], ) + println(io, Printf.@sprintf("time_limit (sec): %6d", algo_params.time_limit)) + end + end + end println(io, "------------------------------------------------------------------------") print(io, "Binary approximation used: ") From 1f17537854858c3f9f577c106e4641796f7ec943 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Wed, 13 Oct 2021 23:17:03 -0400 Subject: [PATCH 17/30] Added some default for binary_precision --- src/algorithmMain.jl | 17 +++++++++++++++++ src/typedefs.jl | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index d3b7c05e..ad5ca2c7 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -59,6 +59,23 @@ function solve( ) end + # Prepare binary_precision + #--------------------------------------------------------------------------- + regime = algo_params.state_approximation_regime + if regime == DynamicSDDiP.BinaryApproximation && isempty(regime.binary_precision) + # If no binary_precision dict has been defined explicitly, it is + # initialized as empty. Then, for each state take a default precision. + for (name, state_comp) in model.nodes[1].states + if JuMP.is_binary(state_comp.out) || JuMP.is_integer(state_comp.out) + regime.binary_precision[name] = 1 + else + ub = JuMP.upper_bound(state_comp.out) + lb = 0 # all states are assumed to satisfy non-negativity constraints + regime.binary_precision[name] = (ub-lb)/7 + end + end + end + # Prepare options for logging #--------------------------------------------------------------------------- options = DynamicSDDiP.Options( diff --git a/src/typedefs.jl b/src/typedefs.jl index e7dd8cfc..e4256145 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -207,7 +207,7 @@ mutable struct BinaryApproximation <: AbstractStateApproximationRegime binary_precision::Dict{Symbol, Float64} cut_projection_regime::AbstractCutProjectionRegime function BinaryApproximation(; - binary_precision = ..., + binary_precision = Dict{Symbol, Float64}(), cut_projection_regime = BigM(), ) return new(binary_precision, cut_projection_regime) @@ -420,8 +420,8 @@ mutable struct NonlinearCut <: Cut cut_variables::Vector{JuMP.VariableRef} cut_constraints::Vector{JuMP.ConstraintRef} ############################################################################ - # obj_y::Union{Nothing,NTuple{N,Float64} where {N}} #TODO - # belief_y::Union{Nothing,Dict{T,Float64} where {T}} #TODO + # obj_y::Union{Nothing,NTuple{N,Float64} where {N}} + # belief_y::Union{Nothing,Dict{T,Float64} where {T}} ############################################################################ non_dominated_count::Int ############################################################################ From 31460c04ed066e4d7145f5817b00e240605347c0 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Wed, 13 Oct 2021 23:17:11 -0400 Subject: [PATCH 18/30] Minor stuff --- src/bellman.jl | 4 +--- src/duals.jl | 4 ++-- src/logging.jl | 2 +- src/state.jl | 1 - stuff.jl | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/bellman.jl b/src/bellman.jl index fd2cade0..71b7598f 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -220,7 +220,7 @@ function refine_bellman_function( ) else # Add a multi-cut @assert bellman_function.cut_type == SDDP.MULTI_CUT - #TODO: Not implemented so far, see SDDP.jl + # TODO: Not implemented so far, see SDDP.jl end end @@ -273,8 +273,6 @@ function _add_average_cut( ############################################################################ # ADD THE CUT USING THE NEW EXPECTED COEFFICIENTS ############################################################################ - # TODO: Hier dann Unterscheidung für state_approximation_regime - _add_cut( node, node.bellman_function.global_theta, diff --git a/src/duals.jl b/src/duals.jl index d398883d..9f2aced0 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -89,7 +89,7 @@ function solve_LP_relaxation( dual_obj = JuMP.objective_value(subproblem) - # Get dual values (reduced costs) for binary states as initial solution # TODO + # Get dual values (reduced costs) for binary states as initial solution get_and_set_dual_values!(node, dual_vars_initial, algo_params.state_approximation_regime) # Note: due to JuMP's dual convention, we need to flip the sign for @@ -464,7 +464,7 @@ function initialize_duals( @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL # or MOI.FEASIBLE_POINT??? - # Get dual values (reduced costs) for binary states as initial solution # TODO + # Get dual values (reduced costs) for binary states as initial solution get_and_set_dual_values!(node, dual_vars_initial, algo_params.state_approximation_regime) # Undo relaxation diff --git a/src/logging.jl b/src/logging.jl index c432f728..1118696d 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -282,7 +282,7 @@ Write the log of the most recent training to a csv for post-analysis. Assumes that the model has been trained via [`DynamicSDDiP.solve`](@ref). """ function write_log_to_csv(model::SDDP.PolicyGraph, filename::String, algo_params::DynamicSDDiP.AlgoParams) - # TODO + # TO-DO end diff --git a/src/state.jl b/src/state.jl index 46b9413b..8536fafd 100644 --- a/src/state.jl +++ b/src/state.jl @@ -40,7 +40,6 @@ end function set_incoming_state!!(node::SDDP.Node, state::Dict{Symbol,Float64}) for (state_name, value) in state - # TODO: Check if required prepare_state_fixing!(node, state_name) # Fix value (bounds are automatically deleted by force argument) diff --git a/stuff.jl b/stuff.jl index 78377458..55519db3 100644 --- a/stuff.jl +++ b/stuff.jl @@ -167,3 +167,41 @@ function _solve_primal_problem( JuMP.set_objective_function(model, primal_obj) return L_λ end + + +using SDDP + +mutable struct DeterministicStopping <: SDDP.AbstractStoppingRule + rtol::Float64 + atol::Float64 + function DeterministicStopping(; + rtol=1e-8, + atol=1e-8 + ) + return new(rtol, atol) + end +end + +v = [DeterministicStopping(), 4, 3.5] + +DeterministicStopping in v + +v[1] == DeterministicStopping() + +isequal(v[1], DeterministicStopping) + +typeof(DeterministicStopping) + +a = v[1] + +a + +a == DeterministicStopping + +isa(a, DeterministicStopping) + +isa(, DeterministicStopping) + + + +contains From 15f71c14fc33b011f2faaa62a5ac5f88ba459fb1 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Wed, 13 Oct 2021 23:17:45 -0400 Subject: [PATCH 19/30] Changed name in project.toml. Don't know why this hasn't been correct after constructing the package. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 108475c7..22b7dad1 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,4 @@ -name = "NCNBD" +name = "DynamicSDDiP" uuid = "76100c40-129e-4398-bc8a-8d92901b12c9" authors = ["Christian Fuellner"] version = "0.1.0" From 61562dba07901c6fb2d0a176a8e004a772a2121c Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 14 Oct 2021 15:10:48 -0400 Subject: [PATCH 20/30] Some minor changes and work on an example --- examples/Others/newExample_1.jl | 145 +++++++ examples/UnitCommitment/InstanceStarter_1.jl | 78 ---- examples/UnitCommitment/UC_2_10.jl | 398 ------------------ examples/UnitCommitment/UC_2_2.jl | 399 ------------------- examples/UnitCommitment/UC_2_5.jl | 394 ------------------ src/DynamicSDDiP.jl | 1 + src/typedefs.jl | 18 +- 7 files changed, 163 insertions(+), 1270 deletions(-) create mode 100644 examples/Others/newExample_1.jl delete mode 100644 examples/UnitCommitment/InstanceStarter_1.jl delete mode 100644 examples/UnitCommitment/UC_2_10.jl delete mode 100644 examples/UnitCommitment/UC_2_2.jl delete mode 100644 examples/UnitCommitment/UC_2_5.jl diff --git a/examples/Others/newExample_1.jl b/examples/Others/newExample_1.jl new file mode 100644 index 00000000..1c47ed7a --- /dev/null +++ b/examples/Others/newExample_1.jl @@ -0,0 +1,145 @@ +# Copyright (c) 2021 Christian Fuellner + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +using JuMP +using SDDP +using DynamicSDDiP +using Revise +using Gurobi +using GAMS +#using SCIP +using Infiltrator + + +function model_config() + + # Stopping rules to be used + stopping_rules = [DynamicSDDiP.DeterministicStopping()] + + # Duality / Cut computation configuration + dual_initialization_regime = DynamicSDDiP.ZeroDuals() + dual_solution_regime = DynamicSDDiP.Kelley() + dual_bound_regime = DynamicSDDiP.BothBounds() + dual_status_regime = DynamicSDDiP.Rigorous() + dual_choice_regime = DynamicSDDiP.StandardChoice() + duality_regime = DynamicSDDiP.LagrangianDuality( + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, + dual_initialization_regime = dual_initialization_regime, + dual_bound_regime = dual_bound_regime, + dual_solution_regime = dual_solution_regime, + dual_choice_regime = dual_choice_regime, + dual_status_regime = dual_status_regime, + ) + + # State approximation and cut projection configuration + cut_projection_regime = DynamicSDDiP.BigM() + binaryPrecision = Dict{Symbol, Float64}() # TODO + + # for (name, state_comp) in model.nodes[1].ext[:lin_states] + # ub = JuMP.upper_bound(state_comp.out) + # + # string_name = string(name) + # if occursin("gen", string_name) + # binaryPrecision[name] = binaryPrecisionFactor * ub + # else + # binaryPrecision[name] = 1 + # end + # end + + state_approximation_regime = DynamicSDDiP.BinaryApproximation( + binary_precision = binary_precision, + cut_projection_regime = cut_projection_regime) + + # Regularization configuration + regularization_regime = DynamicSDDiP.Regularization(sigma = 1, sigma_factor = 5) + + # Cut selection configuration + cut_selection_regime = DynamicSDDiP.NoCutSelection() + + # File for logging + log_file = "C:/Users/cg4102/Documents/julia_logs/UC_2_2_f.log" # TODO + + # Infiltration for debugging + infiltrate_state = :none + + # Definition of algo_params + algo_params = DynamicSDDiP.AlgoParams( + stopping_rules = stopping_rules, + state_approximation_regime = state_approximation_regime, + regularization_regime = regularization_regime, + duality_regime = duality_regime, + cut_selection_regime = cut_selection_regime, + infiltrate_state = infiltrate_state, + log_file = log_file, + ) + + # Define solvers to be used + applied_solvers = DynamicSDDiP.AppliedSolvers( + LP = "Gurobi", + MILP = "Gurobi", + MINLP = "Gurobi", + NLP = "SCIP", + Lagrange = "Gurobi", + ) + + # Start model with used configuration + model_starter( + algo_params, + applied_solvers, + ) +end + + +function model_starter(; + algo_params::DynamicSDDiP.AlgoParams = DynamicSDDiP.AlgoParams(), + applied_solvers::DynamicSDDiP.AppliedSolvers = DynamicSDDiP.AppliedSolvers(), + ) + + ############################################################################ + # DEFINE MODEL + ############################################################################ + model = model_definition() + + ############################################################################ + # SOLVE MODEL + ############################################################################ + DynamicSDDiP.solve(model, algo_params, applied_solvers) +end + + +function model_definition() + + number_of_stages = 2 + + model = SDDP.LinearPolicyGraph( + stages = number_of_stages, + lower_bound = 0.0, + optimizer = GAMS.Optimizer, + sense = :Min + ) do subproblem, t + + ######################################################################## + # DEFINE STAGE-t MODEL + ######################################################################## + + # State variables + JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, Bin, initial_value = 0) + + # Constraints + b = subproblem[:b] + JuMP.@constraint(subproblem, b.out == 1.2 + b.in) + + # Stage objective + SDDP.@stageobjective(subproblem, 1) + + end + + return model +end + +model_config() diff --git a/examples/UnitCommitment/InstanceStarter_1.jl b/examples/UnitCommitment/InstanceStarter_1.jl deleted file mode 100644 index 6addfafe..00000000 --- a/examples/UnitCommitment/InstanceStarter_1.jl +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using GAMS -#using SCIP -using Infiltrator - -include("UC_2_2.jl") -include("UC_2_5.jl") -include("UC_2_10.jl") -include("UC_3_5.jl") -include("UC_3_10.jl") -include("UC_4_5.jl") -include("UC_4_10.jl") -include("UC_5_5.jl") -include("UC_5_10.jl") -include("UC_10_3.jl") -include("UC_10_5.jl") -include("UC_10_10.jl") - -include("UC_2_2_Batt.jl") -include("UC_2_5_Batt.jl") -include("UC_2_10_Batt.jl") -include("UC_3_10_Batt.jl") -include("UC_4_10_Batt.jl") -include("UC_5_5_Batt.jl") -include("UC_10_3_Batt.jl") - - -""" -Used to run different instances of the unit commitment problem (different number -of stages, generators and different paramters) after each other -""" -function start_instances() - - # INSTANCE DEFINITIONS - parameter_sets = [ - [UC_5_5_Batt, 1e-4, 1e-4, :none, :kelley, 0.0, 1e-2, 1e-2], - [UC_5_5_Batt, 1e-4, 1e-4, :none, :bundle_level, 0.2, 1e-2, 1e-2], - - [UC_10_3_Batt, 1e-4, 1e-4, :none, :kelley, 0.0, 1e-2, 1e-2], - [UC_10_3_Batt, 1e-4, 1e-4, :none, :bundle_level, 0.2, 1e-2, 1e-2], - - ] - - for parameter_set in parameter_sets - - module_name = parameter_set[1] - lagrangian_atol = parameter_set[2] - lagrangian_rtol = parameter_set[3] - dual_initialization_regime = parameter_set[4] - lagrangian_method = parameter_set[5] - level_factor = parameter_set[6] - rgap_outer_loop = parameter_set[7] - rgap_inner_loop = parameter_set[8] - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - module_name.unitCommitment_with_parameters( - lagrangian_atol=lagrangian_atol, lagrangian_rtol=lagrangian_rtol, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, level_factor=level_factor, - solvers=solvers, epsilon_outerLoop = rgap_outer_loop, - epsilon_innerLoop = rgap_inner_loop - ) - - end -end - -start_instances() diff --git a/examples/UnitCommitment/UC_2_10.jl b/examples/UnitCommitment/UC_2_10.jl deleted file mode 100644 index a89b76e8..00000000 --- a/examples/UnitCommitment/UC_2_10.jl +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 2 stages and 10 generators -""" - -module UC_2_10 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.236], [0.238], [0.21], [0.226], [0.204], [0.38], [0.416], [0.422], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_2_10() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_2_10_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_2_10() - - generators = [ - Generator(0, 0.0, 1.18, 0.32, 48.94, 0.0, 182.35, 18.0, 0.42, 0.33, -0.21, 1.0, 0.0), - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.05, 0.48, 42.79, 0.0, 171.69, 17.0, 0.21, 0.22, -0.14, 1.02, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 0.72, 1.9, 0.5, 64.06, 0.0, 289.59, 28.0, 0.52, 0.62, -0.5, 1.0, 0.0), - Generator(0, 0.0, 2.08, 0.62, 60.28, 0.0, 286.89, 28.0, 0.67, 0.5, -0.35, 0.95, 0.0), - Generator(1, 0.55, 2.11, 0.55, 66.08, 0.0, 329.89, 33.0, 0.64, 0.69, -0.37, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [8.53 8.02] - - num_of_stages = 2 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_2_2.jl b/examples/UnitCommitment/UC_2_2.jl deleted file mode 100644 index b944d201..00000000 --- a/examples/UnitCommitment/UC_2_2.jl +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 2 stages and 2 generators -""" - -module UC_2_2 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.4], [0.64]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.4], [0.64]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_2_2() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_2_2_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_2_2() - - generators = [ - Generator(0, 0.0, 1.18, 0.32, 48.94, 0.0, 182.35, 18.0, 0.42, 0.33, -0.21, 1.0, 0.0), - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.05, 0.48, 42.79, 0.0, 171.69, 17.0, 0.21, 0.22, -0.14, 1.02, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 0.72, 1.9, 0.5, 64.06, 0.0, 289.59, 28.0, 0.52, 0.62, -0.5, 1.0, 0.0), - Generator(0, 0.0, 2.08, 0.62, 60.28, 0.0, 286.89, 28.0, 0.67, 0.5, -0.35, 0.95, 0.0), - Generator(1, 0.55, 2.11, 0.55, 66.08, 0.0, 329.89, 33.0, 0.64, 0.69, -0.37, 1.02, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [8.53 8.02 7.36 7.31 7.44 8.02 9.58 11.7 13.68 14.28 14.35 14.46 14.29 13.14 13.55 13.66 13.6 13.23 12.71 12.88 13.33 12.56 11.42 10.35] - - num_of_stages = 2 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/examples/UnitCommitment/UC_2_5.jl b/examples/UnitCommitment/UC_2_5.jl deleted file mode 100644 index 53374038..00000000 --- a/examples/UnitCommitment/UC_2_5.jl +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Unit commitment problem with 2 stages and 5 generators -""" - -module UC_2_5 - -export unitCommitment -export unitCommitment_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - # define required tolerances - epsilon_outerLoop = 1e-2 - epsilon_innerLoop = 1e-2 - lagrangian_atol = 1e-4 - lagrangian_rtol = 1e-4 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1000.0] - sigma_factor = 2.0 - - # define initial approximations - plaPrecision = [[0.238], [0.226], [0.204], [0.564], [0.646]] # apart from one generator always 1/5 of pmax - binaryPrecisionFactor = 1/7 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # lagrangian status - lag_status_regime = :lax - # alternatives: :rigorous, :lax - - # outer loop strategy - outer_loop_strategy = :approx - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - unitCommitment_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - lag_status_regime=lag_status_regime, - outer_loop_strategy=outer_loop_strategy, - ) -end - - -function unitCommitment_with_parameters(; - epsilon_outerLoop::Float64 = 1e-2, - epsilon_innerLoop::Float64 = 1e-2, - lagrangian_atol::Float64 = 1e-4, - lagrangian_rtol::Float64 = 1e-4, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1000.0], - sigma_factor::Float64 = 2.0, - plaPrecision::Array{Array{Float64,1},1} = [[0.238], [0.226], [0.204], [0.564], [0.646]], # apart from one generator always 1/5 of pmax - binaryPrecisionFactor::Float64 = 1/7, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["CPLEX", "CPLEX", "Baron", "SCIP", "CPLEX"], - cut_selection::Bool = true, - lag_status_regime::Symbol = :lax, - outer_loop_strategy::Symbol = :approx, - ) - - # DEFINE MODEL - ############################################################################ - model = define_2_5() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = binaryPrecisionFactor * ub - else - binaryPrecision[name] = 1 - end - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection, lag_status_regime, - outer_loop_strategy) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_2_5_f.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_2_5() - - generators = [ - Generator(1, 1.06, 1.19, 0.37, 52.05, 0.0, 177.68, 17.0, 0.31, 0.36, -0.24, 1.0, 0.0), - Generator(0, 0.0, 1.13, 0.48, 53.97, 0.0, 171.60, 17.0, 0.28, 0.27, -0.24, 1.02, 0.0), - Generator(0, 0.0, 1.02, 0.47, 49.45, 0.0, 168.04, 17.0, 0.22, 0.275, -0.17, 1.0, 0.0), - Generator(1, 2.2, 2.82, 0.85, 61.59, 0.0, 486.81, 49.0, 0.9, 0.79, -0.3, 1.1, 0.0), - Generator(0, 0.0, 3.23, 0.84, 54.92, 0.0, 503.34, 50.0, 1.01, 1.00, -0.24, 1.04, 0.0), - ] - num_of_generators = size(generators,1) - - # NOTE: no fixed cost, no fixed emission cost, no o&m cost so far - # NOTE: start-up cost is scaled if less than 24 stages are used, shut-down cost not - - demand_penalty = 5e2 - emission_price = 25 - - demand = [4.27 4.01] - - num_of_stages = 2 - - model = SDDP.LinearPolicyGraph( - stages = num_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - JuMP.@variable(problem, neg_demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - neg_demand_slack = subproblem[:neg_demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - neg_demand_slack = linearizedSubproblem[:neg_demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - return model -end - -end diff --git a/src/DynamicSDDiP.jl b/src/DynamicSDDiP.jl index 2b12b864..125f4ad7 100644 --- a/src/DynamicSDDiP.jl +++ b/src/DynamicSDDiP.jl @@ -32,6 +32,7 @@ include("logging.jl") include("stopping.jl") include("objective.jl") include("bellman.jl") +include("cutSelection.jl") include("solverHandling.jl") include("binarization.jl") include("binaryRefinement.jl") diff --git a/src/typedefs.jl b/src/typedefs.jl index e4256145..a261627b 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -360,7 +360,7 @@ mutable struct AlgoParams log_frequency = 1, log_file = "DynamicSDDiP.log", run_numerical_stability_report = true, - infiltrate_state = :None, + infiltrate_state = :none, ) return new( stopping_rules, @@ -398,6 +398,22 @@ struct AppliedSolvers MINLP :: Any NLP :: Any Lagrange :: Any + + function AppliedSolvers(; + LP = "Gurobi", + MILP = "Gurobi", + MINLP = "Gurobi", + NLP = "SCIP", + Lagrange = "Gurobi", + ) + return new( + LP, + MILP, + MINLP, + NLP, + Lagrange + ) + end end ################################################################################ From 5b7adf5c101f0251d2c3be4153b463fad53eab75 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 14 Oct 2021 15:19:23 -0400 Subject: [PATCH 21/30] Restored old project.toml file instead of the NCNBD one --- Project.toml | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/Project.toml b/Project.toml index 22b7dad1..b3c2cea4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,31 +1,10 @@ name = "DynamicSDDiP" -uuid = "76100c40-129e-4398-bc8a-8d92901b12c9" +uuid = "13dd0c9c-319d-47c7-ae8d-0241c720e43f" authors = ["Christian Fuellner"] version = "0.1.0" -[deps] -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -Debugger = "31a5f54b-26ea-5ae9-a837-f05ce5417438" -Delaunay = "07eb4e4e-0c6d-46ef-bc4e-83d5e5d860a9" -Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" -GAMS = "1ca51c6a-1b4d-4546-9ae1-53e0a243ab12" -Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" -Infiltrator = "5903a43b-9cc3-4c30-8d17-598619ec4e9b" -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -PiecewiseLinearOpt = "0f51c51e-adfa-5141-8a04-d40246b8977c" -Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -Reduce = "93e0c654-6965-5f22-aba9-9c1ae6b3c259" -Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" -SDDP = "f4570300-c277-11e8-125c-4912f86ce65d" -TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" - [compat] -PiecewiseLinearOpt = "0.3" -julia = "1" +julia = "1.5" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" From 608e54adf89e74680ab4a775baf0a758ef73da0b Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 14 Oct 2021 15:27:39 -0400 Subject: [PATCH 22/30] Updated manifest and project toml files --- Manifest.toml | 415 ++++++++++++++++++++++++++++++++++++++++++++++++++ Project.toml | 12 ++ 2 files changed, 427 insertions(+) diff --git a/Manifest.toml b/Manifest.toml index f45eecff..ab1b7291 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,2 +1,417 @@ # This file is machine-generated - editing it directly is not advised +[[Artifacts]] +deps = ["Pkg"] +git-tree-sha1 = "c30985d8821e0cd73870b17b0ed0ce6dc44cb744" +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.3.0" + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BenchmarkTools]] +deps = ["JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"] +git-tree-sha1 = "61adeb0823084487000600ef8b1c00cc2474cd47" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "1.2.0" + +[[Bzip2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "f759de2b2666a8ae52f370f12bf77e984188144d" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.7+0" + +[[CEnum]] +git-tree-sha1 = "215a9aa4a1f23fbd05b92769fdd62559488d70e9" +uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" +version = "0.4.1" + +[[Calculus]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.5.1" + +[[ChainRulesCore]] +deps = ["Compat", "LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "74e8234fb738c45e2af55fdbcd9bfbe00c2446fa" +uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +version = "1.8.0" + +[[CodeTracking]] +deps = ["InteractiveUtils", "UUIDs"] +git-tree-sha1 = "9aa8a5ebb6b5bf469a7e0e2b5202cf6f8c291104" +uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" +version = "1.0.6" + +[[CodecBzip2]] +deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] +git-tree-sha1 = "2e62a725210ce3c3c2e1a3080190e7ca491f18d7" +uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" +version = "0.7.2" + +[[CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.0" + +[[CommonSubexpressions]] +deps = ["MacroTools", "Test"] +git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7" +uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" +version = "0.3.0" + +[[Compat]] +deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] +git-tree-sha1 = "31d0151f5716b655421d9d75b7fa74cc4e744df2" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "3.39.0" + +[[CompilerSupportLibraries_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "8e695f735fca77e9708e795eda62afdb869cbb70" +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "0.3.4+0" + +[[DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "7d9d316f04214f7efdbb6398d545446e246eff02" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.10" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DelimitedFiles]] +deps = ["Mmap"] +uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" + +[[DiffResults]] +deps = ["StaticArrays"] +git-tree-sha1 = "c18e98cba888c6c25d1c3b048e4b3380ca956805" +uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +version = "1.0.3" + +[[DiffRules]] +deps = ["NaNMath", "Random", "SpecialFunctions"] +git-tree-sha1 = "7220bc21c33e990c14f4a9a319b1d242ebc5b269" +uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" +version = "1.3.1" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "a32185f5428d3986f47c2ab78b1f216d5e6cc96f" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.8.5" + +[[ExprTools]] +git-tree-sha1 = "b7e3d17636b348f005f11040025ae8c6f645fe92" +uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" +version = "0.1.6" + +[[FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[ForwardDiff]] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions", "StaticArrays"] +git-tree-sha1 = "63777916efbcb0ab6173d09a658fb7f2783de485" +uuid = "f6369f11-7733-5829-9624-2563aa707210" +version = "0.10.21" + +[[GAMS]] +deps = ["Libdl", "MathOptInterface", "Printf"] +git-tree-sha1 = "50d3b1239a39e88ea187062ab131a3401089789a" +uuid = "1ca51c6a-1b4d-4546-9ae1-53e0a243ab12" +version = "0.2.5" + +[[Gurobi]] +deps = ["CEnum", "Libdl", "MathOptInterface"] +git-tree-sha1 = "aac05324d46b53289ccb05510b05b4a56ffd3ed5" +uuid = "2e9cd046-0924-5485-92f1-d5272153d98b" +version = "0.9.14" + +[[HTTP]] +deps = ["Base64", "Dates", "IniFile", "Logging", "MbedTLS", "NetworkOptions", "Sockets", "URIs"] +git-tree-sha1 = "14eece7a3308b4d8be910e265c724a6ba51a9798" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "0.9.16" + +[[Infiltrator]] +deps = ["REPL"] +git-tree-sha1 = "2418a26375fb0fa5977bf4b2122b787d7097ceb2" +uuid = "5903a43b-9cc3-4c30-8d17-598619ec4e9b" +version = "1.0.3" + +[[IniFile]] +deps = ["Test"] +git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[IrrationalConstants]] +git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151" +uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" +version = "0.1.1" + +[[JLLWrappers]] +deps = ["Preferences"] +git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.3.0" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.2" + +[[JSONSchema]] +deps = ["HTTP", "JSON", "URIs"] +git-tree-sha1 = "2f49f7f86762a0fbbeef84912265a1ae61c4ef80" +uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +version = "0.3.4" + +[[JuMP]] +deps = ["Calculus", "DataStructures", "ForwardDiff", "JSON", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Printf", "Random", "SparseArrays", "SpecialFunctions", "Statistics"] +git-tree-sha1 = "4358b7cbf2db36596bdbbe3becc6b9d87e4eb8f5" +uuid = "4076af6c-e467-56ae-b986-b466b2749572" +version = "0.21.10" + +[[JuliaInterpreter]] +deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] +git-tree-sha1 = "e273807f38074f033d94207a201e6e827d8417db" +uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +version = "0.8.21" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[LogExpFunctions]] +deps = ["ChainRulesCore", "DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] +git-tree-sha1 = "34dc30f868e368f8a17b728a1238f3fcda43931a" +uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" +version = "0.3.3" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[LoweredCodeUtils]] +deps = ["JuliaInterpreter"] +git-tree-sha1 = "491a883c4fef1103077a7f648961adbf9c8dd933" +uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" +version = "2.1.2" + +[[MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "5a5bc6bf062f0f95e62d0fe0a2d99699fed82dd9" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.8" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptInterface]] +deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "JSON", "JSONSchema", "LinearAlgebra", "MutableArithmetics", "OrderedCollections", "SparseArrays", "Test", "Unicode"] +git-tree-sha1 = "575644e3c05b258250bb599e57cf73bbf1062901" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.9.22" + +[[MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] +git-tree-sha1 = "1c38e51c3d08ef2278062ebceade0e46cefc96fe" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.0.3" + +[[MbedTLS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "0eef589dd1c26a3ac9d753fe1a8bcad63f956fa6" +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.16.8+1" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[MutableArithmetics]] +deps = ["LinearAlgebra", "SparseArrays", "Test"] +git-tree-sha1 = "372e3a76d969e651ca70eb647bf0e303bc95d615" +uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" +version = "0.2.21" + +[[NaNMath]] +git-tree-sha1 = "bfe47e760d60b82b66b61d2d44128b62e3a369fb" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "0.3.5" + +[[NetworkOptions]] +git-tree-sha1 = "ed3157f48a05543cce9b241e1f2815f7e843d96e" +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.2.0" + +[[OpenLibm_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "d22054f66695fe580009c09e765175cbf7f13031" +uuid = "05823500-19ac-5b8b-9628-191a04bc5112" +version = "0.7.1+0" + +[[OpenSpecFun_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "9db77584158d0ab52307f8c04f8e7c08ca76b5b3" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.3+4" + +[[OrderedCollections]] +git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.4.1" + +[[Parsers]] +deps = ["Dates"] +git-tree-sha1 = "98f59ff3639b3d9485a03a72f3ab35bab9465720" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.0.6" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Preferences]] +deps = ["TOML"] +git-tree-sha1 = "00cfd92944ca9c760982747e9a1d0d5d86ab1e5a" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.2.2" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Profile]] +deps = ["Printf"] +uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[RecipesBase]] +git-tree-sha1 = "44a75aa7a527910ee3d1751d1f0e4148698add9e" +uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +version = "1.1.2" + +[[Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + +[[Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "4036a3bd08ac7e968e27c203d45f5fff15020621" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.1.3" + +[[Revise]] +deps = ["CodeTracking", "Distributed", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Pkg", "REPL", "Requires", "UUIDs", "Unicode"] +git-tree-sha1 = "41deb3df28ecf75307b6e492a738821b031f8425" +uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" +version = "3.1.20" + +[[SDDP]] +deps = ["Distributed", "HTTP", "JSON", "JuMP", "LinearAlgebra", "MutableArithmetics", "Printf", "Random", "RecipesBase", "Reexport", "SHA", "Statistics", "TimerOutputs"] +git-tree-sha1 = "01ffea8a99d7191d9b83ef3a01711d623973051c" +uuid = "f4570300-c277-11e8-125c-4912f86ce65d" +version = "0.4.3" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[SpecialFunctions]] +deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] +git-tree-sha1 = "793793f1df98e3d7d554b65a107e9c9a6399a6ed" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "1.7.0" + +[[StaticArrays]] +deps = ["LinearAlgebra", "Random", "Statistics"] +git-tree-sha1 = "3c76dde64d03699e074ac02eb2e8ba8254d428da" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "1.2.13" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[TOML]] +deps = ["Dates"] +git-tree-sha1 = "44aaac2d2aec4a850302f9aa69127c74f0c3787e" +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[TimerOutputs]] +deps = ["ExprTools", "Printf"] +git-tree-sha1 = "7cb456f358e8f9d102a8b25e8dfedf58fa5689bc" +uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" +version = "0.5.13" + +[[TranscodingStreams]] +deps = ["Random", "Test"] +git-tree-sha1 = "216b95ea110b5972db65aa90f88d8d89dcb8851c" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.9.6" + +[[URIs]] +git-tree-sha1 = "97bbe755a53fe859669cd907f2d96aee8d2c1355" +uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +version = "1.3.0" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[Zlib_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "320228915c8debb12cb434c59057290f0834dbf6" +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.11+18" diff --git a/Project.toml b/Project.toml index b3c2cea4..191d4e9d 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,18 @@ uuid = "13dd0c9c-319d-47c7-ae8d-0241c720e43f" authors = ["Christian Fuellner"] version = "0.1.0" +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +GAMS = "1ca51c6a-1b4d-4546-9ae1-53e0a243ab12" +Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" +Infiltrator = "5903a43b-9cc3-4c30-8d17-598619ec4e9b" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +SDDP = "f4570300-c277-11e8-125c-4912f86ce65d" +TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" + [compat] julia = "1.5" From 612fa58118f92c3747dacd6ef2fba547f3aef649 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 14 Oct 2021 16:24:55 -0400 Subject: [PATCH 23/30] Corrected compilation errors. --- Project.toml | 1 + src/DynamicSDDiP.jl | 2 +- src/bellman.jl | 16 ++++++++-------- src/binarization.jl | 4 ++-- src/cutSelection.jl | 6 +++--- src/duals.jl | 23 +++++++++++++++++------ src/lagrange.jl | 20 +++++++++++--------- src/logging.jl | 2 +- src/sigmaTest.jl | 4 ++-- src/state.jl | 10 ---------- src/typedefs.jl | 10 +++++----- 11 files changed, 51 insertions(+), 47 deletions(-) diff --git a/Project.toml b/Project.toml index 191d4e9d..50293f53 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" GAMS = "1ca51c6a-1b4d-4546-9ae1-53e0a243ab12" Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" Infiltrator = "5903a43b-9cc3-4c30-8d17-598619ec4e9b" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" diff --git a/src/DynamicSDDiP.jl b/src/DynamicSDDiP.jl index 125f4ad7..0dcc3040 100644 --- a/src/DynamicSDDiP.jl +++ b/src/DynamicSDDiP.jl @@ -24,9 +24,9 @@ using Infiltrator # Write your package code here. include("state.jl") +include("typedefs.jl") include("JuMP.jl") -include("typedefs.jl") include("logging.jl") include("stopping.jl") diff --git a/src/bellman.jl b/src/bellman.jl index 71b7598f..61cb5cfa 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -177,10 +177,10 @@ function refine_bellman_function( # CHECK IF ALL ELEMENTS CONTAINING INFORMATION ON ALL BACKWARD OPENINGS # HAVE THE SAME/REQUIRED LENGTH ############################################################################ - @assert length(dual_variables) == - length(noise_supports) == - length(nominal_probability) == - length(objective_realizations) == + @assert (length(dual_variables) == length(noise_supports) + == length(nominal_probability) + == length(objective_realizations) + ) ############################################################################ # RISK-RELATED PREPARATIOn @@ -613,7 +613,7 @@ function represent_cut_projection_closure!( iteration, beta, ######################################################################## - related_coefficients + related_coefficients, ######################################################################## γ, μ, @@ -671,14 +671,14 @@ function add_complementarity_constraints!( complementarity_constraint_1 = JuMP.@constraint( model, [k=1:K], - ν[k] * γ[k] = 0 + ν[k] * γ[k] == 0 ) append!(cut_constraints, complementarity_constraint_1) complementarity_constraint_2 = JuMP.@constraint( model, [k=1:K], - μ[k] * (γ[k]-1) = 0 + μ[k] * (γ[k]-1) == 0 ) append!(cut_constraints, complementarity_constraint_2) @@ -950,7 +950,7 @@ end function set_up_dict_for_duals( bin_states::Dict{Symbol,BinaryState}, trial_points::Dict{Symbol,Float64}, - state_approximation_regime::DynamicSDDiP.BinaryApproximation + state_approximation_regime::DynamicSDDiP.NoStateApproximation ) return Dict(key => 0.0 for key in keys(trial_points)) diff --git a/src/binarization.jl b/src/binarization.jl index e13726e8..8e9e85f1 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -344,7 +344,7 @@ Determining the anchor points in the original space if BinaryApproximation is used. """ function determine_anchor_states( - node::DynamicSDDiP.Node, + node::SDDP.Node, outgoing_state::Dict{Symbol,Float64}, state_approximation_regime::DynamicSDDiP.BinaryApproximation, ) @@ -395,7 +395,7 @@ Determining the anchor points in the original space if no state approximation is used. """ function determine_anchor_states( - node::DynamicSDDiP.Node, + node::SDDP.Node, outgoing_state::Dict{Symbol,Float64}, state_approximation_regime::DynamicSDDiP.NoStateApproximation, ) diff --git a/src/cutSelection.jl b/src/cutSelection.jl index 00dee3c9..e273acec 100644 --- a/src/cutSelection.jl +++ b/src/cutSelection.jl @@ -20,7 +20,7 @@ Trivial cut selection function if no cut selection is used. function _cut_selection_update( node::SDDP.Node, V::DynamicSDDiP.CutApproximation, - cut::Union{DynamicSDDiP.NonlinearCut,DynamicSDDiP.LinearCut} + cut::Union{DynamicSDDiP.NonlinearCut,DynamicSDDiP.LinearCut}, anchor_state::Dict{Symbol,Float64}, trial_state::Dict{Symbol,Float64}, applied_solvers::DynamicSDDiP.AppliedSolvers, @@ -40,7 +40,7 @@ Simple cut selection feature for nonlinear cuts. function _cut_selection_update( node::SDDP.Node, V::DynamicSDDiP.CutApproximation, - cut::DynamicSDDiP.NonlinearCut + cut::DynamicSDDiP.NonlinearCut, anchor_state::Dict{Symbol,Float64}, trial_state::Dict{Symbol,Float64}, applied_solvers::DynamicSDDiP.AppliedSolvers, @@ -172,7 +172,7 @@ Simple cut selection feature for linear cuts. function _cut_selection_update( node::SDDP.Node, V::DynamicSDDiP.CutApproximation, - cut::DynamicSDDiP.LinearCut + cut::DynamicSDDiP.LinearCut, anchor_state::Dict{Symbol,Float64}, trial_state::Dict{Symbol,Float64}, applied_solvers::DynamicSDDiP.AppliedSolvers, diff --git a/src/duals.jl b/src/duals.jl index 9f2aced0..1825b21a 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -63,6 +63,7 @@ function get_dual_solution( iterations=1, lag_status=nothing, ) +end function solve_LP_relaxation( @@ -101,8 +102,8 @@ function solve_LP_relaxation( undo_relax() return( - dual_obj = dual_obj - dual_vars = dual_vars_initial + dual_obj = dual_obj, + dual_vars = dual_vars_initial, ) end @@ -285,7 +286,7 @@ function get_dual_solution( iterations=lag_iterations, lag_status=lag_status, ) - +end #******************************************************************************* # AUXILIARY METHODS @@ -487,7 +488,7 @@ function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Flo end function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Float64}, - state_approximation_regime::DynamicSDDiP.NoState) + state_approximation_regime::DynamicSDDiP.NoStateApproximation) for (i, name) in enumerate(keys(node,states)) reference_to_constr = FixRef(name.in) @@ -499,7 +500,7 @@ end function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, - integrality_handler::SDDP.SDDiP, state_approximation_regime::DynamicSDDiP.BinaryApproximation) + state_approximation_regime::DynamicSDDiP.BinaryApproximation) for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) dual_values[name] = dual_vars[i] @@ -515,7 +516,7 @@ end function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, - integrality_handler::SDDP.SDDiP, state_approximation_regime::DynamicSDDiP.NoStateApproximation) + state_approximation_regime::DynamicSDDiP.NoStateApproximation) for (i, name) in enumerate(keys(node.states)) dual_values[name] = dual_vars[i] @@ -523,3 +524,13 @@ function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, return end + +function get_number_of_states(node::SDDP.Node, state_approximation_regime::DynamicSDDiP.BinaryApproximation) + + return length(node.ext[:backward_data][:bin_states]) +end + +function get_number_of_states(node::SDDP.Node, state_approximation_regime::DynamicSDDiP.NoStateApproximation) + + return length(node.states) +end diff --git a/src/lagrange.jl b/src/lagrange.jl index cedc1fbb..d18a8b87 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -58,7 +58,7 @@ function _solve_Lagrangian_relaxation!( # Set the Lagrangian relaxation of the objective in the primal model old_obj = JuMP.objective_function(model) - JuMP.set_objective_function(model, @expression(old_obj - π_k' * h_expr)) + JuMP.set_objective_function(model, @expression(model, old_obj - π_k' * h_expr)) # Optimization JuMP.optimize!(model) @@ -85,7 +85,7 @@ function solve_lagrangian_dual( node_index::Int64, primal_obj::Float64, π_k::Vector{Float64}, - bound_results::Tuple{Float64,Float64} + bound_results::Tuple{Float64,Float64}, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.Kelley @@ -155,7 +155,7 @@ function solve_lagrangian_dual( @variable(approx_model, π⁺[1:number_of_states] >= 0) @variable(approx_model, π⁻[1:number_of_states] >= 0) @expression(approx_model, π, π⁺ .- π⁻) # not required to be a constraint - set_multiplier_bounds!(approx_model, number_of_states bound_results.dual_bound) + set_multiplier_bounds!(approx_model, number_of_states, bound_results.dual_bound) ############################################################################ # CUTTING-PLANE METHOD @@ -221,6 +221,7 @@ function solve_lagrangian_dual( # sometimes this occurs due to numerical issues # still leads to a valid cut lag_status = :conv + end elseif all(h_k .== 0) # NO OPTIMALITY ACHIEVED, BUT STILL ALL SUBGRADIENTS ARE ZERO # may occur due to numerical issues @@ -314,7 +315,7 @@ end function restore_copy_constraints!( node::SDDP.Node, - x_in_value::Vector{Float64} + x_in_value::Vector{Float64}, state_approximation_regime::DynamicSDDiP.BinaryApproximation, ) @@ -328,7 +329,7 @@ end function restore_copy_constraints!( node::SDDP.Node, - x_in_value::Vector{Float64} + x_in_value::Vector{Float64}, state_approximation_regime::DynamicSDDiP.NoStateApproximation, ) @@ -365,7 +366,7 @@ achieved in total. function magnanti_wong!( node::SDDP.Node, - approx_model::JuMP.model, + approx_model::JuMP.Model, π_k::Vector{Float64}, π_star::Vector{Float64}, t_k::Float64, @@ -408,7 +409,7 @@ end function magnanti_wong!( node::SDDP.Node, - approx_model::JuMP.model, + approx_model::JuMP.Model, π_k::Vector{Float64}, π_star::Vector{Float64}, t_k::Float64, @@ -436,7 +437,7 @@ function solve_lagrangian_dual( node_index::Int64, primal_obj::Float64, π_k::Vector{Float64}, - bound_results::Tuple{Float64,Float64} + bound_results::Tuple{Float64,Float64}, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.LevelBundle @@ -509,7 +510,7 @@ function solve_lagrangian_dual( @variable(approx_model, π⁺[1:number_of_states] >= 0) @variable(approx_model, π⁻[1:number_of_states] >= 0) @expression(approx_model, π, π⁺ .- π⁻) # not required to be a constraint - set_multiplier_bounds!(approx_model, number_of_states bound_results.dual_bound) + set_multiplier_bounds!(approx_model, number_of_states, bound_results.dual_bound) ############################################################################ # CUTTING-PLANE METHOD @@ -654,6 +655,7 @@ function solve_lagrangian_dual( # sometimes this occurs due to numerical issues # still leads to a valid cut lag_status = :conv + end elseif all(h_k .== 0) # NO OPTIMALITY ACHIEVED, BUT STILL ALL SUBGRADIENTS ARE ZERO # may occur due to numerical issues diff --git a/src/logging.jl b/src/logging.jl index 1118696d..12d7d1b3 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -267,7 +267,7 @@ function print_footer(io, training_results) flush(io) end -function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log_file_handle::Any, log::DynamicSDDiP.log) +function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log_file_handle::Any, log::DynamicSDDiP.Log) if algo_params.print_level > 0 && mod(length(log), algo_params.log_frequency) == 0 print_helper(print_iteration, log_file_handle, log[end]) end diff --git a/src/sigmaTest.jl b/src/sigmaTest.jl index 1a835ea7..52dcb796 100644 --- a/src/sigmaTest.jl +++ b/src/sigmaTest.jl @@ -58,7 +58,7 @@ function forward_sigma_test( incoming_state_value, # only values, no State struct! noise, scenario_path[1:depth], - algo_params.infiltrate_state + algo_params.infiltrate_state, algo_params.regularization_regime, ) end @@ -76,7 +76,7 @@ function forward_sigma_test( incoming_state_value, # only values, no State struct! noise, scenario_path[1:depth], - algo_params.infiltrate_state + algo_params.infiltrate_state, DynamicSDDiP.NoRegularization, ) end diff --git a/src/state.jl b/src/state.jl index 8536fafd..645d764d 100644 --- a/src/state.jl +++ b/src/state.jl @@ -146,13 +146,3 @@ struct BinaryState x_name::Symbol # name of original state it is related to k::Int64 # index and exponent end - -function get_number_of_states(node::SDDP.Node, state_approximation_regime::DynamicSDDiP.BinaryApproximation) - - return length(node.ext[:backward_data][:bin_states]) -end - -function get_number_of_states(node::SDDP.Node, state_approximation_regime::DynamicSDDiP.NoStateApproximation) - - return length(node.states) -end diff --git a/src/typedefs.jl b/src/typedefs.jl index a261627b..399c4cf1 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -249,8 +249,6 @@ Default is Regularization. ################################################################################ abstract type AbstractDualityRegime end -mutable struct LagrangianDuality <: AbstractDualityRegime end - mutable struct LagrangianDuality <: AbstractDualityRegime atol::Float64 rtol::Float64 @@ -301,6 +299,7 @@ mutable struct CutSelection <: AbstractCutSelectionRegime ) return new(cut_deletion_minimum) end +end mutable struct NoCutSelection <: AbstractCutSelectionRegime end @@ -328,7 +327,7 @@ mutable struct AlgoParams duality_regime::AbstractDualityRegime cut_selection_regime::AbstractCutSelectionRegime ############################################################################ - risk_measure = SDDP.Expectation() + risk_measure::SDDP.AbstractRiskMeasure forward_pass::SDDP.AbstractForwardPass sampling_scheme::SDDP.AbstractSamplingScheme backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme @@ -349,6 +348,7 @@ mutable struct AlgoParams regularization_regime = Regularization(), duality_regime = LagrangianDuality(), cut_selection_regime = CutSelection(), + risk_measure = SDDP.Expectation(), forward_pass = SDDP.DefaultForwardPass(), sampling_scheme = SDDP.InSampleMonteCarlo(), backward_sampling_scheme = SDDP.CompleteSampler(), @@ -378,7 +378,7 @@ mutable struct AlgoParams print_level, log_frequency, log_file, - run_numerical_stability_report + run_numerical_stability_report, infiltrate_state, ) end @@ -428,7 +428,7 @@ mutable struct NonlinearCut <: Cut ############################################################################ trial_state::Dict{Symbol,Float64} anchor_state::Dict{Symbol,Float64} - binary_state::Dict{Symbol,BinaryState} + binary_state::Dict{Symbol,DynamicSDDiP.BinaryState} binary_precision::Dict{Symbol,Float64} ############################################################################ sigma::Float64 From c0576ec87c92a506264ff703ec2057dab60e764a Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 15 Oct 2021 14:23:36 -0400 Subject: [PATCH 24/30] Deleted NCNBD examples --- examples/Others/Ex_3.jl | 313 ------------------------------ examples/Others/GAMS_test.jl | 32 --- examples/Others/GAMS_test_2.jl | 47 ----- examples/Others/SDDPTest.jl | 32 --- examples/Others/UnitCommitment.jl | 279 -------------------------- examples/Others/discontExample.jl | 128 ------------ examples/Others/fifthExample.jl | 130 ------------- examples/Others/firstExample.jl | 100 ---------- examples/Others/fourthExample.jl | 201 ------------------- examples/Others/newExample_1.jl | 145 -------------- examples/Others/secondExample.jl | 127 ------------ examples/Others/sixthExample.jl | 137 ------------- examples/Others/test.jl | 301 ---------------------------- examples/Others/thirdExample.jl | 207 -------------------- 14 files changed, 2179 deletions(-) delete mode 100644 examples/Others/Ex_3.jl delete mode 100644 examples/Others/GAMS_test.jl delete mode 100644 examples/Others/GAMS_test_2.jl delete mode 100644 examples/Others/SDDPTest.jl delete mode 100644 examples/Others/UnitCommitment.jl delete mode 100644 examples/Others/discontExample.jl delete mode 100644 examples/Others/fifthExample.jl delete mode 100644 examples/Others/firstExample.jl delete mode 100644 examples/Others/fourthExample.jl delete mode 100644 examples/Others/newExample_1.jl delete mode 100644 examples/Others/secondExample.jl delete mode 100644 examples/Others/sixthExample.jl delete mode 100644 examples/Others/test.jl delete mode 100644 examples/Others/thirdExample.jl diff --git a/examples/Others/Ex_3.jl b/examples/Others/Ex_3.jl deleted file mode 100644 index a6d00b9b..00000000 --- a/examples/Others/Ex_3.jl +++ /dev/null @@ -1,313 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -module Ex_3 - -export thirdExample -export thirdExample_with_parameters - -using JuMP -using SDDP -using NCNBD -using Revise -#using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function thirdExample() - - # define required tolerances - epsilon_outerLoop = 1e-3 - epsilon_innerLoop = 1e-3 #1e-4 - lagrangian_atol = 1e-8 - lagrangian_rtol = 1e-8 - - # define time and iteration limits - lagrangian_iteration_limit = 1000 - iteration_limit = 1000 - time_limit = 10800 - - # define sigma - sigma = [0.0, 1.0] - sigma_factor = 5.0 - - # define initial approximations - plaPrecision = [2.0, 4.0] - binaryPrecisionFactor = 0.5 - - # define infiltration level - infiltrate_state = :none - # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - - # define regime for initializing duals for Lagrangian relaxation - dual_initialization_regime = :zeros - # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - - # define solution method for lagrangian dual - lagrangian_method = :kelley - # alternatives: :kelley, :bundle_proximal, :bundle_level - - bundle_alpha = 0.5 - bundle_factor = 1.0 - level_factor = 0.2 - - # cut selection strategy - cut_selection = true - - # used solvers - solvers = ["CPLEX", "CPLEX", "Baron", "Baron", "CPLEX"] - - # CALL METHOD WITH PARAMETERS - ############################################################################ - thirdExample_with_parameters( - epsilon_outerLoop=epsilon_outerLoop, - epsilon_innerLoop=epsilon_innerLoop, - lagrangian_atol=lagrangian_atol, - lagrangian_rtol=lagrangian_rtol, - lagrangian_iteration_limit=lagrangian_iteration_limit, - iteration_limit=iteration_limit, - time_limit=time_limit, - sigma=sigma, - sigma_factor=sigma_factor, - plaPrecision=plaPrecision, - binaryPrecisionFactor=binaryPrecisionFactor, - infiltrate_state=infiltrate_state, - dual_initialization_regime=dual_initialization_regime, - lagrangian_method=lagrangian_method, - bundle_alpha=bundle_alpha, - bundle_factor=bundle_factor, - level_factor=level_factor, - solvers=solvers, - cut_selection=cut_selection, - ) -end - - -function thirdExample_with_parameters(; - epsilon_outerLoop::Float64 = 1e-3, - epsilon_innerLoop::Float64 = 1e-3, - lagrangian_atol::Float64 = 1e-8, - lagrangian_rtol::Float64 = 1e-8, - lagrangian_iteration_limit::Int = 1000, - iteration_limit::Int=1000, - time_limit::Int = 10800, - sigma::Vector{Float64} = [0.0, 1.0], - sigma_factor::Float64 = 5.0, - plaPrecision::Vector{Float64} = [2.0, 4.0], - binaryPrecisionFactor::Float64 = 0.5, - infiltrate_state::Symbol = :none, # alternatives: :none, :all, :outer, :sigma, :inner, :lagrange, :bellman - dual_initialization_regime::Symbol = :zeros, # alternatives: :zeros, :gurobi_relax, :cplex_relax, :cplex_fixed, :cplex_combi - lagrangian_method::Symbol = :kelley, # alternatives: :kelley, :bundle_proximal, :bundle_level - bundle_alpha::Float64 = 0.5, - bundle_factor::Float64 = 1.0, - level_factor::Float64 = 0.2, - solvers::Vector{String} = ["Gurobi", "Gurobi", "Baron", "Baron", "Gurobi"], - cut_selection::Bool = true, - ) - - # DEFINE MODEL - ############################################################################ - model = define_thirdExample() - - # DEFINE SOLVERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(solvers[1], solvers[2], solvers[3], solvers[4], solvers[5]) - - # DEFINE INITIAL APPROXIMATIONS - ############################################################################ - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - binaryPrecision[name] = binaryPrecisionFactor - end - - # SET-UP PARAMETER STRUCTS - ############################################################################ - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_factor, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, lagrangian_method, - bundle_alpha, bundle_factor, level_factor, - cut_selection) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_factor, - infiltrate_state, lagrangian_atol, - lagrangian_rtol, lagrangian_iteration_limit, - dual_initialization_regime, - lagrangian_method, bundle_alpha, - bundle_factor, level_factor, - cut_selection) - - # SOLVE MODEL - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = iteration_limit, print_level = 2, - time_limit = time_limit, stopping_rules = [NCNBD.DeterministicStopping()], - log_file = "C:/Users/cg4102/Documents/julia_logs/ThirdEx.log") - - # WRITE LOGS TO FILE - ############################################################################ - #NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - - -function define_thirdExample() - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable(subproblem, 0.0 <= x <= 2.0, SDDP.State, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= x <= 2.0, NCNBD.State, initial_value = 0) - - # DEFINE STAGE 1 MODEL - ######################################################################## - if t == 1 - - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - x = problem[:x] - JuMP.@variable(problem, 0.0 <= y_loc <= 2.0) - JuMP.@constraint(problem, lin_con, y_loc + 3/2*x.out >= 3 + x.in) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, nonlinearAux[1:numberOfNonlinearFunctions]) - JuMP.@constraint(problem, actual_nlcon, y_loc - nonlinearAux[1] >= 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - y_loc = subproblem[:y_loc] - SDDP.@stageobjective(subproblem, y_loc) - - y_loc = linearizedSubproblem[:y_loc] - NCNBD.@lin_stageobjective(linearizedSubproblem, y_loc) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # user-defined function for evaluation - nlf_eval = function nonl_function_1_eval(y::Float64) - return sqrt(y) - end - - # user-defined function for expression building - nlf_expr = function nonl_function_1_expr(y::JuMP.VariableRef) - return :(sqrt($(y))) - end - - # define nonlinear expression - x = subproblem[:x] - nonlinear_exp = nlf_expr(x.out) - - # nonlinear constraint - nonlinearAux = subproblem[:nonlinearAux] - JuMP.add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - x = linearizedSubproblem[:x] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf = NCNBD.NonlinearFunction(nlf_eval, nlf_expr, nonlinearAux[1], [x.out], :shift, :keep) - push!(nonlinearFunctionList, nlf) - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - # DEFINE STAGE 2 MODEL - ######################################################################## - else - - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - x = problem[:x] - JuMP.@variable(problem, 0.0 <= y_loc[i=1:2] <= 4.0) - JuMP.@constraint(problem, lin_con, sum(y_loc[i] for i in 1:2) == 2 * x.in) - JuMP.@constraint(problem, x.out == 0) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, nonlinearAux[1:numberOfNonlinearFunctions]) - JuMP.@constraint(problem, actual_nlcon, y_loc[1] - nonlinearAux[1] >= 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - y_loc = subproblem[:y_loc] - SDDP.@stageobjective(subproblem, y_loc[1]) - - y_loc = linearizedSubproblem[:y_loc] - NCNBD.@lin_stageobjective(linearizedSubproblem, y_loc[1]) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # user-defined function for evaluation - nlf_eval = function nonl_function_2_eval(y::Float64) - return y^2 - end - - # user-defined function for expression building - nlf_expr = function nonl_function_2_expr(y::JuMP.VariableRef) - return :($(y)^2) - end - - # define nonlinear expression - y_loc = subproblem[:y_loc] - nonlinear_exp = nlf_expr(y_loc[2]) - - # nonlinear constraint - nonlinearAux = subproblem[:nonlinearAux] - JuMP.add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - y_loc = linearizedSubproblem[:y_loc] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf = NCNBD.NonlinearFunction(nlf_eval, nlf_expr, nonlinearAux[1], [y_loc[2]], :shift, :keep) - push!(nonlinearFunctionList, nlf) - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - end - - return model -end - -end diff --git a/examples/Others/GAMS_test.jl b/examples/Others/GAMS_test.jl deleted file mode 100644 index 9be0d99a..00000000 --- a/examples/Others/GAMS_test.jl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using GAMS -using JuMP -using Gurobi -using MathOptInterface - -m1 = JuMP.Model(GAMS.Optimizer) -JuMP.set_optimizer_attribute(m1, "Solver", "Gurobi") -#JuMP.set_silent(m1) -JuMP.@variable(m1, x>=0) -JuMP.@objective(m1, MOI.MIN_SENSE, x) -JuMP.@constraint(m1, x >= 2257.812325) -JuMP.optimize!(m1) - -m2 = JuMP.Model(Gurobi.Optimizer) -#JuMP.set_silent(m2) -JuMP.@variable(m2, y>=0) -JuMP.@objective(m2, MOI.MIN_SENSE, y) -JuMP.@constraint(m2, y >= 2257.812325) - -JuMP.optimize!(m2) - -println() -println("Optimal point calling Gurobi via GAMS:") -println(JuMP.value(x)) -println("Optimal point calling Gurobi directly:") -println(JuMP.value(y)) diff --git a/examples/Others/GAMS_test_2.jl b/examples/Others/GAMS_test_2.jl deleted file mode 100644 index e7776cc1..00000000 --- a/examples/Others/GAMS_test_2.jl +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using GAMS -using JuMP -using BenchmarkTools - -dual_vars = zeros(8) - -approx_model = JuMP.Model(GAMS.Optimizer) -JuMP.set_optimizer_attribute(approx_model, "Solver", "Gurobi") - -@variables approx_model begin - θ - x[1:length(dual_vars)] -end -JuMP.@objective(approx_model, JuMP.MOI.MAX_SENSE, θ) - -JuMP.@constraint( - approx_model, - θ - 0.65 * x[3] + x[4] - x[5] + x[6] + x[7] <= 36 -) - -JuMP.optimize!(approx_model) -dual_vars .= value.(x) - -println() -println(JuMP.value(x[1])) -println(JuMP.value(x[2])) -println(JuMP.value(x[3])) -println(JuMP.value(x[4])) -println(JuMP.value(x[5])) -println(JuMP.value(x[6])) -println(JuMP.value(x[7])) -println(JuMP.value(x[8])) - -println(dual_vars) - -#@btime dual_vars[findall(isnan.(dual_vars))] .= 0.0 -@btime replace!(dual_vars, NaN => 0) - -println(dual_vars) - -println(@__FILE__) diff --git a/examples/Others/SDDPTest.jl b/examples/Others/SDDPTest.jl deleted file mode 100644 index 8b8256b8..00000000 --- a/examples/Others/SDDPTest.jl +++ /dev/null @@ -1,32 +0,0 @@ -using SDDP, GLPK - -model = SDDP.LinearPolicyGraph( - stages = 1, - sense = :Max, - upper_bound = 500.0, - optimizer = GLPK.Optimizer -) do subproblem, t - # Define the state variable. - @variable(subproblem, 0 <= volume <= 200, SDDP.State, initial_value = 200) - # Define the control variables. - @variables(subproblem, begin - thermal_generation >= 0 - hydro_generation >= 0 - hydro_spill >= 0 - end) - # Define the constraints - @constraints(subproblem, begin - volume.out == volume.in - hydro_generation - hydro_spill - thermal_generation + hydro_generation == 150.0 - end) - # Define the objective for each stage `t`. Note that we can use `t` as an - # index for t = 1, 2, 3. - fuel_cost = [10.0] - @stageobjective(subproblem, fuel_cost[t] * thermal_generation) -end - -println(model.nodes[1].subproblem) - -println(model.nodes[1].stage_objective) - -SDDP.train(model; iteration_limit = 10) diff --git a/examples/Others/UnitCommitment.jl b/examples/Others/UnitCommitment.jl deleted file mode 100644 index 40ad8fde..00000000 --- a/examples/Others/UnitCommitment.jl +++ /dev/null @@ -1,279 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -struct Generator - comm_ini::Int - gen_ini::Float64 - pmax::Float64 - pmin::Float64 - fuel_cost::Float64 - om_cost::Float64 - su_cost::Float64 - sd_cost::Float64 - ramp_up::Float64 - ramp_dw::Float64 - a::Float64 - b::Float64 - c::Float64 -end - - -function unitCommitment() - - generators = [ - Generator(0, 0.0, 200.0, 40.0, 18.0, 2.0, 42.6, 42.6, 40.0, 40.0, -2.375, 1025.0, 0.0), - #Generator(0, 0.0, 320.0, 64.0, 15.0, 4.0, 50.6, 50.6, 64.0, 64.0, -2.75, 1800.0, 0.0), - #Generator(0, 0.0, 150.0, 30.0, 17.0, 2.0, 57.1, 57.1, 30.0, 30.0, -3.2, 1025.0, 0.0), - #Generator(1, 400.0, 520.0, 104.0, 13.2, 4.0, 47.1, 47.1, 104.0, 104.0, -1.5, 1800.0, 0.0), - #Generator(1, 280.0, 280.0, 56.0, 14.3, 4.0, 56.9, 56.9, 56.0, 56.0, -3, 1800.0, 0.0), - #Generator(0, 0.0, 80.0, 16.0, 40.2, 4.0, 141.5, 141.5, 30.0, 30.0, -6.8, 1200.0, 0.0), - #Generator(1, 120.0, 120.0, 24.0, 17.1, 2.0, 113.5, 113.5, 24.0, 24.0, -4, 1025.0, 0.0), - #Generator(1, 110.0, 110.0, 22.0, 17.3, 2.0, 42.6, 42.6, 22.0, 22.0, -4.75, 1025.0, 0.0), - #Generator(0, 0.0, 80.0, 16.0, 59.4, 4.0, 50.6, 50.6, 16.0, 16.0, -6.0, 1200.0, 0.0), - #Generator(0, 0.0, 60.0, 12.0, 19.5, 2.0, 57.1, 57.1, 12.0, 12.0, -8.5, 1025.0, 0.0), - ] - num_of_generators = size(generators,1) - - demand_penalty = 5e4 - emission_price = 0.02 #0.02 €/kg = 20 €/t - - demand = [40 60 1010 1149 1236 1331 1397 1419 1455 1455 1441 1419 1397 1339 1368 1339 1236 1105 1038 959 922 885 915 834] - #883 915 - # 40 60 - # 104 180 - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable( - subproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - SDDP.State, - Bin, - initial_value = generators[i].comm_ini - ) - JuMP.@variable( - linearizedSubproblem, - 0.0 <= commit[i = 1:num_of_generators] <= 1.0, - NCNBD.State, - Bin, - initial_value = generators[i].comm_ini - ) - - JuMP.@variable( - subproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - SDDP.State, - initial_value = generators[i].gen_ini - ) - - JuMP.@variable( - linearizedSubproblem, - 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, - NCNBD.State, - initial_value = generators[i].gen_ini - ) - - # DEFINE STAGE t MODEL - ######################################################################## - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = num_of_generators - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - gen = problem[:gen] - commit = problem[:commit] - - # start-up variables - JuMP.@variable(problem, up[i=1:num_of_generators], Bin) - JuMP.@variable(problem, down[i=1:num_of_generators], Bin) - - # demand slack - JuMP.@variable(problem, demand_slack >= 0.0) - - # cost variables - JuMP.@variable(problem, startup_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, shutdown_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, fuel_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, om_costs[i=1:num_of_generators] >= 0.0) - JuMP.@variable(problem, emission_costs[i=1:num_of_generators] >= 0.0) - - # generation bounds - JuMP.@constraint(problem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) - JuMP.@constraint(problem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) - - # ramping - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up) - JuMP.@constraint(problem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw) - - # start-up and shut-down - # we do not need a case distinction as we defined initial_values - JuMP.@constraint(problem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) - JuMP.@constraint(problem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) - - # load balance - JuMP.@constraint(problem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack == demand[t] ) - - # costs - JuMP.@constraint(problem, startupcost[i=1:num_of_generators], generators[i].su_cost * up[i] == startup_costs[i]) - JuMP.@constraint(problem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) - JuMP.@constraint(problem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) - JuMP.@constraint(problem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, emission_aux[1:num_of_generators]) - JuMP.@constraint(problem, emissioncost[i=1:num_of_generators], emission_price * emission_aux[i] == emission_costs[i]) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - su_costs = subproblem[:startup_costs] - sd_costs = subproblem[:shutdown_costs] - f_costs = subproblem[:fuel_costs] - om_costs = subproblem[:om_costs] - em_costs = subproblem[:emission_costs] - demand_slack = subproblem[:demand_slack] - SDDP.@stageobjective(subproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty) - - su_costs = linearizedSubproblem[:startup_costs] - sd_costs = linearizedSubproblem[:shutdown_costs] - f_costs = linearizedSubproblem[:fuel_costs] - om_costs = linearizedSubproblem[:om_costs] - em_costs = linearizedSubproblem[:emission_costs] - demand_slack = linearizedSubproblem[:demand_slack] - NCNBD.@lin_stageobjective(linearizedSubproblem, - sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] + em_costs[i] for i in 1:num_of_generators) - + demand_slack * demand_penalty) - - @infiltrate - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # TODO: Add c*commit, but then two-dimensional - # TODO: Use same function only ones, but insert correct paramters - - nlf_emission_eval = - - for i in 1:num_of_generators - # user-defined function for evaluation - nlf_emission_eval = function nonl_function_eval(y::Float64) - return generators[i].b * y + generators[i].a * y^2 - end - - # user-defined function for expression building - nlf_emission_expr = function nonl_function_expr(y::JuMP.VariableRef) - return :($(generators[i].b) * $(y) + $(generators[i].a) * $(y)^2) - end - - # define nonlinear expression - gen = subproblem[:gen][i] - nonlinear_exp = nlf_emission_expr(gen.out) - - # nonlinear constraint - aux = subproblem[:emission_aux][i] - JuMP.add_NL_constraint(subproblem, :($(aux) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - gen = linearizedSubproblem[:gen][i] - aux = linearizedSubproblem[:emission_aux][i] - - nlf = NCNBD.NonlinearFunction(nlf_emission_eval, nlf_emission_expr, aux, [gen.out], :noshift, :replace) - push!(nonlinearFunctionList, nlf) - - end - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - # SET-UP PARAMETERS - ############################################################################ - # applied_solvers = NCNBD.AppliedSolvers(GAMS.Optimizer, GAMS.Optimizer, GAMS.Optimizer, GAMS.Optimizer) - applied_solvers = NCNBD.AppliedSolvers("Gurobi", "Gurobi", "Baron", "Baron") - - epsilon_outerLoop = 1e-3 - epsilon_innerLoop = 1e-4 - - binaryPrecision = Dict{Symbol, Float64}() - - for (name, state_comp) in model.nodes[1].ext[:lin_states] - ub = JuMP.upper_bound(state_comp.out) - - string_name = string(name) - if occursin("gen", string_name) - binaryPrecision[name] = 1/7 * ub - else - binaryPrecision[name] = 1/5 - end - end - - @infiltrate - - plaPrecision = [40, 64, 30, 104, 56, 20, 24, 22, 16, 12] # apart from one generator always 1/5 of pmax - sigma = [0.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, - 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, - 10.0, 10.0, 10.0, 10.0] - sigma_counter = 5 - - @infiltrate - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_counter) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_counter) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 15, print_level = 1, - time_limit = 7200, stopping_rules = [NCNBD.DeterministicStopping()]) - - #@infiltrate - - # WRITE LOGS TO FILE - ############################################################################ - NCNBD.write_log_to_csv(model, "uc_results.csv", algoParameters) - -end - -unitCommitment() diff --git a/examples/Others/discontExample.jl b/examples/Others/discontExample.jl deleted file mode 100644 index bad65a58..00000000 --- a/examples/Others/discontExample.jl +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function discontExample() - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= b <= 2.0, NCNBD.State, initial_value = 0) - - # DEFINE STAGE 1 MODEL - ######################################################################## - if t == 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - b = problem[:b] - JuMP.@constraint(problem, b.out == 1.2 + b.in) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - SDDP.@stageobjective(subproblem, 1) - NCNBD.@lin_stageobjective(linearizedSubproblem, 1) - - # store in ext of subproblem - nonlinearFunctionList = NCNBD.NonlinearFunction[] - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - # DEFINE STAGE 2 MODEL - ######################################################################## - else - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - b = problem[:b] - JuMP.@variable(problem, 0.0 <= x[i=1:4]) - JuMP.set_integer(x[1]) - JuMP.set_integer(x[2]) - - JuMP.@constraint(problem, con, 1.25*x[1] - x[2] + 0.5*x[3] + 1/3*x[4] == b.in) - JuMP.@constraint(problem, b.out == 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - x = subproblem[:x] - SDDP.@stageobjective(subproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - - x = linearizedSubproblem[:x] - NCNBD.@lin_stageobjective(linearizedSubproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - - # store in ext of subproblem - nonlinearFunctionList = NCNBD.NonlinearFunction[] - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - end - - # SET-UP PARAMETERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(Gurobi.Optimizer, Gurobi.Optimizer, GAMS.Optimizer) - - epsilon_outerLoop = 0.01 - epsilon_innerLoop = 0.001 - binaryPrecision = Dict(:b => 2/63) - plaPrecision = [0, 0] - sigma = [0.0, 1.0] - sigma_counter = 5 - - #@infiltrate - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_counter) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_counter) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 5, print_level = 1, - time_limit = 6000, stopping_rules = [NCNBD.DeterministicStopping()]) - - #@infiltrate - - # WRITE LOGS TO FILE - ############################################################################ - NCNBD.write_log_to_csv(model, "discont_results.csv", algoParameters) - -end - -discontExample() diff --git a/examples/Others/fifthExample.jl b/examples/Others/fifthExample.jl deleted file mode 100644 index 43d69aac..00000000 --- a/examples/Others/fifthExample.jl +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function fifthExample() - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, Bin, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= b <= 2.0, NCNBD.State, Bin, initial_value = 0) - - # DEFINE STAGE 1 MODEL - ######################################################################## - if t == 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - b = problem[:b] - JuMP.@constraint(problem, b.out == 1.2 + b.in) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - SDDP.@stageobjective(subproblem, 1) - NCNBD.@lin_stageobjective(linearizedSubproblem, 1) - - # store in ext of subproblem - nonlinearFunctionList = NCNBD.NonlinearFunction[] - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - # DEFINE STAGE 2 MODEL - ######################################################################## - else - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - b = problem[:b] - JuMP.@variable(problem, 0.0 <= x[i=1:4]) - JuMP.set_integer(x[1]) - JuMP.set_integer(x[2]) - - JuMP.@constraint(problem, con, 1.25*x[1] - x[2] + 0.5*x[3] + 1/3*x[4] == b.in) - JuMP.@constraint(problem, b.out == 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - x = subproblem[:x] - SDDP.@stageobjective(subproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - - x = linearizedSubproblem[:x] - NCNBD.@lin_stageobjective(linearizedSubproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - - # store in ext of subproblem - nonlinearFunctionList = NCNBD.NonlinearFunction[] - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - end - - # SET-UP PARAMETERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(GAMS.Optimizer, GAMS.Optimizer, GAMS.Optimizer) - - epsilon_outerLoop = 0.01 - epsilon_innerLoop = 0.001 - binaryPrecision = Dict(:b => 2/63) - plaPrecision = [0, 0] - sigma = [0.0, 1.0] - sigma_counter = 5 - - #@infiltrate - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_counter) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_counter) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 5, print_level = 1, - time_limit = 6000, stopping_rules = [NCNBD.DeterministicStopping()]) - - #@infiltrate - - # WRITE LOGS TO FILE - ############################################################################ - NCNBD.write_log_to_csv(model, "fifth_example.csv", algoParameters) - -end - -fifthExample() - -# example related to discontExample but with binary state diff --git a/examples/Others/firstExample.jl b/examples/Others/firstExample.jl deleted file mode 100644 index 8f237b09..00000000 --- a/examples/Others/firstExample.jl +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using Delaunay -using Gurobi -using PiecewiseLinearOpt -using Revise -using NCNBD - -function main() - - # (1) SET-UP THE ORIGINAL MINLP MODEL - ############################################################################ - MINLPmodel = JuMP.Model() - @variable(MINLPmodel, 0 <= x[i=1:2] <= 4, base_name="x") - @objective(MINLPmodel, Min, x[1] + x[2]) - set_optimizer(MINLPmodel, Gurobi.Optimizer) - - # nonlinear functions to be used (first defined, then added using expression graph) - # -------------------------------------------------------------------------- - # first a list is constructed containing all the nonlinear expressions and function structs - #nonlinearExpressionList = NonlinearExpression[] - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 2 - - # for each nonlinear term to be approximated, an auxiliary variables is introduced - # should be multidimensional in general - @variable(MINLPmodel, nonlinearAux[i=1:numberOfNonlinearFunctions], base_name="nonlinearAux") - - # for each nonlinear constraint, the nonlinear term is replaced by the auxiliary variable - @constraint(MINLPmodel, actualnlcon_1, x[2] - nonlinearAux[1] <= 0) - @constraint(MINLPmodel, actualnlcon_2, -x[2] + nonlinearAux[2] <= 0) - - # the original (still linear) model is copied - # note that this is done this early, since nonlinear models cannot be copied in JuMP - # note that the variables have to be copied as well to get a reference to the new model - MILPmodel = JuMP.copy(MINLPmodel) - x_MILP = copy(x, MILPmodel) - - # defining and registering the nonlinear functions - # registration is required for using user-defined nonlinear functions in JuMP - nonlinearexp_1(y) = y^2 - nonlinearexp_2(y) = sqrt(y) - #nonlinearExpressionList[1] = nonlinearexp_1 - #nonlinearExpressionList[2] = nonlinearexp_2 - - register(MINLPmodel, :nonlinearexp_1, 1, nonlinearexp_1, autodiff=true) - register(MINLPmodel, :nonlinearexp_2, 1, nonlinearexp_2, autodiff=true) - #for i in nonlinearExpressionList: - #register(MINLPmodel; Symbol("nonlinearexp_$i"), 1, nonlinearExpressionList[i], autodiff=true) - - # defining the nonlinear constraints by setting the auxiliary variable equal to the nonlinear term - @NLconstraint(MINLPmodel, nlcon_1, nonlinearAux[1]==nonlinearexp_1(x[2])) - nlf_1 = NCNBD.NonlinearFunction([x_MILP[2]], nonlinearAux[1], nlcon_1, nonlinearexp_1) - push!(nonlinearFunctionList, nlf_1) - - @NLconstraint(MINLPmodel, nlcon_2, nonlinearAux[2]==nonlinearexp_2(x[2])) - nlf_2 = NCNBD.NonlinearFunction([x_MILP[2]], nonlinearAux[2], nlcon_2, nonlinearexp_2) - push!(nonlinearFunctionList, nlf_2) - - # (2) DEFINE NC-NBD parameters - ############################################################################ - sigma = 1.0 - initialSimplices = 5 - initialBinaryPrecision = 0.5 - maxcuts = 100 - - algo_params = NCNBD.InitialAlgoParams(sigma, initialSimplices, initialBinaryPrecision, maxcuts) - - # (3) START THE NC-NBD METHOD - ############################################################################ - NCNBD.startNCNBD(MINLPmodel, MILPmodel, nonlinearFunctionList, algo_params) - - - - # get information on model - #num_variables(MINLPmodel) # number of all variables - #num_constraints(MINLPmodel) # number of (linear) constraints - #num_nl_constraints(MINLPmodel) # number of nonlinear constraints - #all_variables(MINLPmodel) # all variables in model - #quadr - #quadr(1) - #model[:x] - #model[:nlcon] - #nlcon - -end - -#function quadr( -# y::Float64 -#) -# return y^2 -#end - - -main() diff --git a/examples/Others/fourthExample.jl b/examples/Others/fourthExample.jl deleted file mode 100644 index 46f8b8b4..00000000 --- a/examples/Others/fourthExample.jl +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function fourthExample() - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable(subproblem, 0.0 <= x <= 2.0, SDDP.State, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= x <= 2.0, NCNBD.State, initial_value = 0) - - # DEFINE STAGE 1 MODEL - ######################################################################## - if t == 1 - - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - x = problem[:x] - JuMP.@variable(problem, 0.0 <= y_loc <= 2.0) - JuMP.@constraint(problem, lin_con, y_loc + 3/2*x.out >= 3 + x.in) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, nonlinearAux[1:numberOfNonlinearFunctions]) - JuMP.@constraint(problem, actual_nlcon, y_loc - nonlinearAux[1] >= 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - y_loc = subproblem[:y_loc] - SDDP.@stageobjective(subproblem, y_loc) - - y_loc = linearizedSubproblem[:y_loc] - NCNBD.@lin_stageobjective(linearizedSubproblem, y_loc) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # user-defined function for evaluation - nlf_eval = function nonl_function_1_eval(y::Float64) - return sqrt(y) - end - - # user-defined function for expression building - nlf_expr = function nonl_function_1_expr(y::JuMP.VariableRef) - return :(sqrt($(y))) - end - - # define nonlinear expression - x = subproblem[:x] - nonlinear_exp = nlf_expr(x.out) - - # nonlinear constraint - nonlinearAux = subproblem[:nonlinearAux] - JuMP.add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - x = linearizedSubproblem[:x] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf = NCNBD.NonlinearFunction(nlf_eval, nlf_expr, nonlinearAux[1], [x.out], :replace) - push!(nonlinearFunctionList, nlf) - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - # DEFINE STAGE 2 MODEL - ######################################################################## - else - - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - x = problem[:x] - JuMP.@variable(problem, 0.0 <= y_loc[i=1:2] <= 4.0) - JuMP.@constraint(problem, lin_con, sum(y_loc[i] for i in 1:2) == 2 * x.in) - JuMP.@constraint(problem, x.out == 0) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, nonlinearAux[1:numberOfNonlinearFunctions]) - JuMP.@constraint(problem, actual_nlcon, 2 - nonlinearAux[1] >= 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - y_loc = subproblem[:y_loc] - SDDP.@stageobjective(subproblem, y_loc[1]) - - y_loc = linearizedSubproblem[:y_loc] - NCNBD.@lin_stageobjective(linearizedSubproblem, y_loc[1]) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # user-defined function for evaluation - nlf_eval = function nonl_function_2_eval(x::Float64, y::Float64) - return x*y - end - - # user-defined function for expression building - nlf_expr = function nonl_function_2_expr(x::JuMP.VariableRef, y::JuMP.VariableRef) - return :($(x)*$(y)) - end - - # define nonlinear expression - y_loc = subproblem[:y_loc] - nonlinear_exp = nlf_expr(y_loc[1], y_loc[2]) - - # nonlinear constraint - nonlinearAux = subproblem[:nonlinearAux] - JuMP.add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - y_loc = linearizedSubproblem[:y_loc] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf = NCNBD.NonlinearFunction(nlf_eval, nlf_expr, nonlinearAux[1], [y_loc[1], y_loc[2]], :keep) - push!(nonlinearFunctionList, nlf) - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - end - - # SET-UP PARAMETERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(GAMS.Optimizer, GAMS.Optimizer, GAMS.Optimizer) - - epsilon_outerLoop = 1e-3 - epsilon_innerLoop = 1e-4 - binaryPrecision = Dict(:x => 0.5) - plaPrecision = [2, 4] - sigma = [0.0, 1.0] - sigma_counter = 5 - - #@infiltrate - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_counter) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_counter) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 2, print_level = 1, - time_limit = 6000, stopping_rules = [NCNBD.DeterministicStopping()]) - - #@infiltrate - - # WRITE LOGS TO FILE - ############################################################################ - NCNBD.write_log_to_csv(model, "fourth_example.csv", algoParameters) - -end - -fourthExample() - -# example related to thirdExample but with two-dimensional nonlinearity diff --git a/examples/Others/newExample_1.jl b/examples/Others/newExample_1.jl deleted file mode 100644 index 1c47ed7a..00000000 --- a/examples/Others/newExample_1.jl +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using DynamicSDDiP -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function model_config() - - # Stopping rules to be used - stopping_rules = [DynamicSDDiP.DeterministicStopping()] - - # Duality / Cut computation configuration - dual_initialization_regime = DynamicSDDiP.ZeroDuals() - dual_solution_regime = DynamicSDDiP.Kelley() - dual_bound_regime = DynamicSDDiP.BothBounds() - dual_status_regime = DynamicSDDiP.Rigorous() - dual_choice_regime = DynamicSDDiP.StandardChoice() - duality_regime = DynamicSDDiP.LagrangianDuality( - atol = 1e-8, - rtol = 1e-8, - iteration_limit = 1000, - dual_initialization_regime = dual_initialization_regime, - dual_bound_regime = dual_bound_regime, - dual_solution_regime = dual_solution_regime, - dual_choice_regime = dual_choice_regime, - dual_status_regime = dual_status_regime, - ) - - # State approximation and cut projection configuration - cut_projection_regime = DynamicSDDiP.BigM() - binaryPrecision = Dict{Symbol, Float64}() # TODO - - # for (name, state_comp) in model.nodes[1].ext[:lin_states] - # ub = JuMP.upper_bound(state_comp.out) - # - # string_name = string(name) - # if occursin("gen", string_name) - # binaryPrecision[name] = binaryPrecisionFactor * ub - # else - # binaryPrecision[name] = 1 - # end - # end - - state_approximation_regime = DynamicSDDiP.BinaryApproximation( - binary_precision = binary_precision, - cut_projection_regime = cut_projection_regime) - - # Regularization configuration - regularization_regime = DynamicSDDiP.Regularization(sigma = 1, sigma_factor = 5) - - # Cut selection configuration - cut_selection_regime = DynamicSDDiP.NoCutSelection() - - # File for logging - log_file = "C:/Users/cg4102/Documents/julia_logs/UC_2_2_f.log" # TODO - - # Infiltration for debugging - infiltrate_state = :none - - # Definition of algo_params - algo_params = DynamicSDDiP.AlgoParams( - stopping_rules = stopping_rules, - state_approximation_regime = state_approximation_regime, - regularization_regime = regularization_regime, - duality_regime = duality_regime, - cut_selection_regime = cut_selection_regime, - infiltrate_state = infiltrate_state, - log_file = log_file, - ) - - # Define solvers to be used - applied_solvers = DynamicSDDiP.AppliedSolvers( - LP = "Gurobi", - MILP = "Gurobi", - MINLP = "Gurobi", - NLP = "SCIP", - Lagrange = "Gurobi", - ) - - # Start model with used configuration - model_starter( - algo_params, - applied_solvers, - ) -end - - -function model_starter(; - algo_params::DynamicSDDiP.AlgoParams = DynamicSDDiP.AlgoParams(), - applied_solvers::DynamicSDDiP.AppliedSolvers = DynamicSDDiP.AppliedSolvers(), - ) - - ############################################################################ - # DEFINE MODEL - ############################################################################ - model = model_definition() - - ############################################################################ - # SOLVE MODEL - ############################################################################ - DynamicSDDiP.solve(model, algo_params, applied_solvers) -end - - -function model_definition() - - number_of_stages = 2 - - model = SDDP.LinearPolicyGraph( - stages = number_of_stages, - lower_bound = 0.0, - optimizer = GAMS.Optimizer, - sense = :Min - ) do subproblem, t - - ######################################################################## - # DEFINE STAGE-t MODEL - ######################################################################## - - # State variables - JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, Bin, initial_value = 0) - - # Constraints - b = subproblem[:b] - JuMP.@constraint(subproblem, b.out == 1.2 + b.in) - - # Stage objective - SDDP.@stageobjective(subproblem, 1) - - end - - return model -end - -model_config() diff --git a/examples/Others/secondExample.jl b/examples/Others/secondExample.jl deleted file mode 100644 index e17afe49..00000000 --- a/examples/Others/secondExample.jl +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -using SCIP -using Infiltrator - - -function exampleModel() - - model = SDDP.LinearPolicyGraph( - stages = 1, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - #integrality_handler = SDDP.SDDiP() - ) do subproblem, t - - # DEFINE MILP MODEL - ######################################################################## - linearizedSubproblem = JuMP.Model() - - # construct a list to store all nonlinear functions - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 2 - - # SET-UP LINEAR PART OF MODEL - ######################################################################## - for problem in [subproblem, linearizedSubproblem] - @variable(problem, 0 <= x[i=1:2] <= 4) - - # SET-UP EXPRESSION GRAPH FOR NONLINEARITIES OF MINLP MODEL - #################################################################### - # for each nonlinear term, introduce an auxiliary variable - @variable(problem, nonlinearAux[i=1:numberOfNonlinearFunctions]) - - # for each nonlinear constraint, determine the expression graph - # and replace the nonlinearity by the auxliary variable - @constraint(problem, actual_nlcon_1, x[2] - nonlinearAux[1] <= 0) - @constraint(problem, actual_nlcon_2, -x[2] + nonlinearAux[2] <= 0) - end - - x = subproblem[:x] - @stageobjective(subproblem, sum(x[i] for i in 1:2)) - - x = linearizedSubproblem[:x] - @objective(linearizedSubproblem, MOI.MIN_SENSE, sum(x[i] for i in 1:2)) - - # SET-UP NONLINEARITIES - ######################################################################## - # define nonlinear functions as user-defined functions - # (once for evaluation, once for expression building) - nlf_1_eval = function nonlinear_function_1_eval(y::Float64) - return y^2 - end - - nlf_1_expr = function nonlinear_function_1_expr(y::JuMP.VariableRef) - return :($(y)^2) - end - - nlf_2_eval = function nonlinear_function_2_eval(y::Float64, z::Float64) - return sqrt(y) + sqrt(z) - end - - nlf_2_expr = function nonlinear_function_2_expr(y::JuMP.VariableRef, z::JuMP.VariableRef) - return :(sqrt($(y)) + sqrt($(z))) - end - - # define nonlinear expressions (once as Julia expression) - x = subproblem[:x] - nonlinearexp_1 = nlf_1_expr(x[2]) - nonlinearexp_2 = nlf_2_expr(x[1], x[2]) - - # defining nonlinear constraints using auxiliary variables - nonlinearAux = subproblem[:nonlinearAux] - add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinearexp_1))) - add_NL_constraint(subproblem, :($(nonlinearAux[2]) == $(nonlinearexp_2))) - - # construct nonlinearFunction objects for both constraints - x = linearizedSubproblem[:x] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf_1 = NCNBD.NonlinearFunction(nlf_1_eval, nlf_1_expr, nonlinearAux[1], [x[2]]) - nlf_2 = NCNBD.NonlinearFunction(nlf_2_eval, nlf_2_expr, nonlinearAux[2], [x[1], x[2]]) - - # push both nonlinearFunction objects to list - push!(nonlinearFunctionList, nlf_1) - push!(nonlinearFunctionList, nlf_2) - - # no access to model or node yet, so store nonlinearFunctionList - # and the linearizedSubproblem in ext of subproblem - # shift it to right location later - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - - # SET-UP PARAMETERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(Gurobi.Optimizer, Gurobi.Optimizer, GAMS.Optimizer) - - epsilon_outerLoop = 0.0 - epsilon_innerLoop = 0.0 - binaryPrecision = [0.5] - plaPrecision = [0.5] - sigma = [1.0] - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, sigma) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 100, print_level = 0) - -end - -exampleModel() diff --git a/examples/Others/sixthExample.jl b/examples/Others/sixthExample.jl deleted file mode 100644 index 9940e382..00000000 --- a/examples/Others/sixthExample.jl +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function sixthExample() - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= b <= 2.0, NCNBD.State, initial_value = 0) - - JuMP.@variable(subproblem, 0.0 <= c <= 1.0, SDDP.State, Bin, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= c <= 1.0, NCNBD.State, Bin, initial_value = 0) - - # DEFINE STAGE 1 MODEL - ######################################################################## - if t == 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - b = problem[:b] - JuMP.@constraint(problem, b.out == 1.2 + b.in) - c = problem[:c] - JuMP.@constraint(problem, c.out == 1 + c.in) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - SDDP.@stageobjective(subproblem, 1) - NCNBD.@lin_stageobjective(linearizedSubproblem, 1) - - # store in ext of subproblem - nonlinearFunctionList = NCNBD.NonlinearFunction[] - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - # DEFINE STAGE 2 MODEL - ######################################################################## - else - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - b = problem[:b] - c = problem[:c] - JuMP.@variable(problem, 0.0 <= x[i=1:4]) - JuMP.set_integer(x[1]) - JuMP.set_integer(x[2]) - - JuMP.@constraint(problem, con, 1.25*x[1] - x[2] + 0.5*x[3] + 1/3*x[4] + c.in*2/3 == b.in) - JuMP.@constraint(problem, b.out == 0) - JuMP.@constraint(problem, c.out == 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - x = subproblem[:x] - SDDP.@stageobjective(subproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - - x = linearizedSubproblem[:x] - NCNBD.@lin_stageobjective(linearizedSubproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - - # store in ext of subproblem - nonlinearFunctionList = NCNBD.NonlinearFunction[] - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - end - - # SET-UP PARAMETERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(GAMS.Optimizer, GAMS.Optimizer, GAMS.Optimizer) - - epsilon_outerLoop = 0.01 - epsilon_innerLoop = 0.001 - binaryPrecision = Dict(:b => 1/7, :c => 1) - plaPrecision = [0, 0] - sigma = [0.0, 1.0] - sigma_counter = 5 - - #@infiltrate - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_counter) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_counter) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 5, print_level = 1, - time_limit = 6000, stopping_rules = [NCNBD.DeterministicStopping()]) - - #@infiltrate - - # WRITE LOGS TO FILE - ############################################################################ - NCNBD.write_log_to_csv(model, "sixth_example.csv", algoParameters) - -end - -sixthExample() - -# example related to discontExample but with two state variables diff --git a/examples/Others/test.jl b/examples/Others/test.jl deleted file mode 100644 index 22d08140..00000000 --- a/examples/Others/test.jl +++ /dev/null @@ -1,301 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using SCIP -using GAMS - -function exampleModelTest() - - # normal nonlinear constraint - ############################################################################ - # storing: nonlinear expression cannot be stored for later purposes - # evaluating: nonlinear expression cannot be evaluated later - # solvers: solvers can handle the constraint - # optimal value: 1.0, optimal point: [1.0, 1.0] - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # @NLconstraint(problem, x[1]*x[2] <= 1) - # print(problem) - # JuMP.optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - # mrapo julialang example. - # nonlinear expression as user-defined function - ############################################################################ - # storing: nonlinear expression can be stored for later purposes - # evaluating: nonlinear expression can be evaluated later - # solvers: solvers cannot handle the constraint - # LoadError: unrecognized operation (nonlinearexp) - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # nonlinearexp(x) = x[1]*x[2] - # register(problem, :nonlinearexp, 2, nonlinearexp, autodiff=true) - # add_NL_constraint(problem, :(nonlinearexp($(x...)) <= 1)) - # JuMP.optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - # odow julialang example. - # nonlinear expression as user-defined function - ############################################################################ - # storing: nonlinear expression can be stored for later purposes - # evaluating: nonlinear expression can be evaluated later - # solvers: solvers cannot handle the constraint - # LoadError: unrecognized operation (nonlinearexp) - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # nonlinearexp(x...) = x[1]*x[2] - # register(problem, :nonlinearexp, 2, nonlinearexp, autodiff=true) - # @NLconstraint(problem, nonlinearexp(x[1], x[2]) <= 1) - # JuMP.optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - # my own example. - # nonlinear expression as user-defined function - ############################################################################ - # storing: nonlinear expression can be stored for later purposes - # evaluating: nonlinear expression can be evaluated later - # solvers: solvers cannot handle the constraint - # LoadError: unrecognized operation (nonlinearexp) - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # nonlinearexp(x,y) = x*y - # register(problem, :nonlinearexp, 2, nonlinearexp, autodiff=true) - # @NLconstraint(problem, nonlinearexp(x[1], x[2]) <= 1) - # print(problem) - # JuMP.optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - # nonlinear expression as Julia expression - ############################################################################ - # storing: nonlinear expression can be stored for later purposes - # evaluating: nonlinear expression cannot be evaluated due to inequality - # solvers: solvers can handle the constraint - # optimal value: 1.0, optimal point: [1.0, 1.0] - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # nonlinear expression can be stored and solvers can handle it - # However, it cannot be evaluated as it is an inequality - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # expressionStored = :($(x[1])*$(x[2]) <= 1) - # JuMP.add_NL_constraint(problem, expressionStored) - # print(problem) - # JuMP.optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - # # nonlinear expression as combined expression - ############################################################################ - # storing: nonlinear expression can be stored for later purposes - # evaluating: nonlinear expression cannot be evaluated - # (at least I don't know how to in the multidimensional case) - # solvers: solvers can handle the constraint - # optimal value: 1.0, optimal point: [1.0, 1.0] - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # By splits into several expressions, we also obtain an evaluatable one. - # However, I do not know how to evaluate this in the multidimensional case. - # I tried one version (do not remember which), which worked, but only - # using x..., i.e. all components of x, and not only the ones used in the term. - # Moreover, many of those x... approaches did not work, so I did not manage - # to figure out the right one again. - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # express1 = :($(x[1])*$(x[2])) - # express2 = :($(express1) <= 1) - # add_NL_constraint(problem, express2) - # print(problem) - # optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - # # nonlinear expression as expression and related user-defined function - ############################################################################ - # storing: nonlinear expression can be stored for later purposes - # evaluating: nonlinear expression cannot be evaluated - # (at least I don't know how to in the multidimensional case) - # solvers: solvers can handle the constraint - # optimal value: 1.0, optimal point: [1.0, 1.0] - # => NOT SUFFICIENT FOR MY PURPOSE - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # express1 = :($(x[1])*$(x[2])) - # express2 = :($(express1) <= 1) - # @eval expressFunction(x) = $express1 - # add_NL_constraint(problem, express2) - # print(problem) - # optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - ############################################################################ - # println(expressFunction(1)) -> JUST RETURNS THE EXPRESSION - # println(expressFunction(1,2)) -> ERROR - # println(expressFunction([1 2])) -> JUST RETURNS THE EXPRESSION - # The problem may be that eval always runs in the global scope. - - # # nonlinear expression as expression and independent user-defined function - ############################################################################ - # storing: nonlinear expression can be stored for later purposes (as expression) - # evaluating: nonlinear expression can be evaluated (as user-defined function) - # solvers: solvers can handle the constraint (as expression) - # optimal value: 1.0, optimal point: [1.0, 1.0] - # => SUFFICIENT FOR MY PURPOSE BUT VERY CHUNKY - # NOT SUFFICIENT, SINCE THE EXPRESSION CANNOT BE RE-USED IN ANOTHER - # MODEL AS x[1] AND x[2] ARE BOUND TO THIS MODEL - ############################################################################ - # problem = JuMP.Model(GAMS.Optimizer) - # JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - # @variable(problem, 0 <= x[i=1:2] <= 1) - # @objective(problem, Max, x[2]) - # @constraint(problem, x[2] - x[1] <= 0) - # express1 = :($(x[1])*$(x[2])) - # express2 = :($(express1) <= 1) - # add_NL_constraint(problem, express2) - # print(problem) - # optimize!(problem) - # println() - # println("optimal value: ", objective_value(problem)) - # println("optimal point: ", value.(x)) - # println() - - problem = JuMP.Model(GAMS.Optimizer) - JuMP.set_optimizer_attribute(problem, "Solver", "Ipopt") - @variable(problem, 0 <= x[i=1:2] <= 1) - @objective(problem, Max, x[2]) - @constraint(problem, x[2] - x[1] <= 0) - #register(problem, :nonlinearexp, 2, nonlinearexp, autodiff=true) - println() - express1 = :($(x[1])*$(x[2])) - express2 = :($(express1) <= 1) - JuMP.add_NL_constraint(problem, express2) - - function christian(y::JuMP.VariableRef) - return :(sqrt($(y))) - end - a = function christian(y::Float64) - return sqrt(y) - end - - function christian2(x::JuMP.VariableRef, y::JuMP.VariableRef) - return :($(x)*$(y)) - end - function christian2(x::Float64, y::Float64) - return x*y - end - - function christian3(y::JuMP.VariableRef) - return :($(y)^2) - end - - expi = christian(x[1]) - expo = christian(x[2]) - - println(expi) - println(typeof(expi)) - println(expo) - println(typeof(expo)) - - problem2 = JuMP.Model(GAMS.Optimizer) - JuMP.set_optimizer_attribute(problem2, "Solver", "Ipopt") - @variable(problem2, 0 <= y[i=1:2] <= 1) - - expa = christian(y[1]) - println(expa) - println(typeof(expa)) - println(christian(2.0)) - - expe = christian2(x[1], x[2]) - println(expe) - println(typeof(expe)) - - expu = christian3(x[2]) - println(expu) - println(typeof(expu)) - - println(a(3.0)) - - b = NCNBD.solve_ncnbd - println(b) - # b(2) - - #println(express2) - #println(typeof(express2)) - #println(problem) - #nlexp2 = :($(christian(x[1])) <= 1) - #nlexp3 = :($(nlexp2)) - #println(nlexp3) - #println(typeof(nlexp3)) - - add_NL_constraint(problem, :($(expi) <= 1)) - add_NL_constraint(problem, :($(expu) <= 1)) - #print(problem) - add_NL_constraint(problem2, :($(expa) <= 1)) - #print(problem2) - JuMP.optimize!(problem) - println() - #println("optimal value: ", objective_value(problem)) - #println("optimal point: ", value.(x)) - println() -#nlcon_4 = add_NL_constraint(subproblem, :($(Expr(:call, :nonlinearexp_2, x[2], x[1])) == $(nonlinearAux[2]) )) - -end - - -exampleModelTest() diff --git a/examples/Others/thirdExample.jl b/examples/Others/thirdExample.jl deleted file mode 100644 index 874a6c96..00000000 --- a/examples/Others/thirdExample.jl +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (c) 2021 Christian Fuellner - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -using JuMP -using SDDP -using NCNBD -using Revise -using Gurobi -using GAMS -#using SCIP -using Infiltrator - - -function thirdExample() - - model = SDDP.LinearPolicyGraph( - stages = 2, - lower_bound = 0.0, - optimizer = Gurobi.Optimizer, - sense = :Min - ) do subproblem, t - - # DEFINE LINEARIZED PROBLEM (MILP) - # ------------------------------------------------------------------ - linearizedSubproblem = JuMP.Model() - node = subproblem.ext[:sddp_node] - model = subproblem.ext[:sddp_policy_graph] - linearizedSubproblem.ext[:sddp_node] = node - linearizedSubproblem.ext[:sddp_policy_graph] = model - # place holder for state variable refs of linearized problem - node.ext[:lin_states] = Dict{Symbol,NCNBD.State{JuMP.VariableRef}}() - model.ext[:lin_initial_root_state] = Dict{Symbol,Float64}() - - # DEFINE STATE VARIABLES - # ------------------------------------------------------------------ - JuMP.@variable(subproblem, 0.0 <= x <= 2.0, SDDP.State, initial_value = 0) - JuMP.@variable(linearizedSubproblem, 0.0 <= x <= 2.0, NCNBD.State, initial_value = 0) - - # DEFINE STAGE 1 MODEL - ######################################################################## - if t == 1 - - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - x = problem[:x] - JuMP.@variable(problem, 0.0 <= y_loc <= 2.0) - JuMP.@constraint(problem, lin_con, y_loc + 3/2*x.out >= 3 + x.in) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, nonlinearAux[1:numberOfNonlinearFunctions]) - JuMP.@constraint(problem, actual_nlcon, y_loc - nonlinearAux[1] >= 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - y_loc = subproblem[:y_loc] - SDDP.@stageobjective(subproblem, y_loc) - - y_loc = linearizedSubproblem[:y_loc] - NCNBD.@lin_stageobjective(linearizedSubproblem, y_loc) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # user-defined function for evaluation - nlf_eval = function nonl_function_1_eval(y::Float64) - return sqrt(y) - end - - # user-defined function for expression building - nlf_expr = function nonl_function_1_expr(y::JuMP.VariableRef) - return :(sqrt($(y))) - end - - # define nonlinear expression - x = subproblem[:x] - nonlinear_exp = nlf_expr(x.out) - - # nonlinear constraint - nonlinearAux = subproblem[:nonlinearAux] - JuMP.add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - x = linearizedSubproblem[:x] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf = NCNBD.NonlinearFunction(nlf_eval, nlf_expr, nonlinearAux[1], [x.out], :replace) - push!(nonlinearFunctionList, nlf) - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - # DEFINE STAGE 2 MODEL - ######################################################################## - else - - # DEFINE STORAGE FOR NONLINEAR DATA - # ------------------------------------------------------------------ - nonlinearFunctionList = NCNBD.NonlinearFunction[] - numberOfNonlinearFunctions = 1 - - # DEFINE LINEAR PART OF MODEL - # ------------------------------------------------------------------ - for problem in [subproblem, linearizedSubproblem] - x = problem[:x] - JuMP.@variable(problem, 0.0 <= y_loc[i=1:2] <= 4.0) - JuMP.@constraint(problem, lin_con, sum(y_loc[i] for i in 1:2) == 2 * x.in) - JuMP.@constraint(problem, x.out == 0) - - # DEFINE EXPRESSION GRAPH FOR NONLINEAR CONSTRAINT - # -------------------------------------------------------------- - JuMP.@variable(problem, nonlinearAux[1:numberOfNonlinearFunctions]) - JuMP.@constraint(problem, actual_nlcon, y_loc[1] - nonlinearAux[1] >= 0) - end - - # DEFINE STAGE OBJECTIVE - # ------------------------------------------------------------------ - y_loc = subproblem[:y_loc] - SDDP.@stageobjective(subproblem, y_loc[1]) - - y_loc = linearizedSubproblem[:y_loc] - NCNBD.@lin_stageobjective(linearizedSubproblem, y_loc[1]) - - # DEFINE NONLINEARITY - # ------------------------------------------------------------------ - # user-defined function for evaluation - nlf_eval = function nonl_function_2_eval(y::Float64) - return y^2 - end - - # user-defined function for expression building - nlf_expr = function nonl_function_2_expr(y::JuMP.VariableRef) - return :($(y)^2) - end - - # define nonlinear expression - y_loc = subproblem[:y_loc] - nonlinear_exp = nlf_expr(y_loc[2]) - - # nonlinear constraint - nonlinearAux = subproblem[:nonlinearAux] - JuMP.add_NL_constraint(subproblem, :($(nonlinearAux[1]) == $(nonlinear_exp))) - - # define nonlinearFunction struct for PLA - y_loc = linearizedSubproblem[:y_loc] - nonlinearAux = linearizedSubproblem[:nonlinearAux] - nlf = NCNBD.NonlinearFunction(nlf_eval, nlf_expr, nonlinearAux[1], [y_loc[2]], :keep) - push!(nonlinearFunctionList, nlf) - - # store in ext of subproblem - subproblem.ext[:nlFunctions] = nonlinearFunctionList - subproblem.ext[:linSubproblem] = linearizedSubproblem - - end - end - - # SET-UP PARAMETERS - ############################################################################ - applied_solvers = NCNBD.AppliedSolvers(GAMS.Optimizer, GAMS.Optimizer, GAMS.Optimizer) - - epsilon_outerLoop = 1e-3 - epsilon_innerLoop = 1e-4 - binaryPrecision = Dict(:x => 0.5) - plaPrecision = [2, 4] - sigma = [0.0, 1.0] - sigma_counter = 5 - - #@infiltrate - - initialAlgoParameters = NCNBD.InitialAlgoParams(epsilon_outerLoop, - epsilon_innerLoop, binaryPrecision, plaPrecision, - sigma, sigma_counter) - algoParameters = NCNBD.AlgoParams(epsilon_outerLoop, epsilon_innerLoop, - binaryPrecision, sigma, sigma_counter) - - # SET-UP NONLINEARITIES - ############################################################################ - NCNBD.solve(model, algoParameters, initialAlgoParameters, applied_solvers, - iteration_limit = 5, print_level = 1, - time_limit = 6000, stopping_rules = [NCNBD.DeterministicStopping()]) - - #@infiltrate - - # WRITE LOGS TO FILE - ############################################################################ - NCNBD.write_log_to_csv(model, "test_results.csv", algoParameters) - -end - -thirdExample() - - -# NOTE: Solving the model in GAMS with BARON gives the optimal solution: -# v* = 2.3732 -# x_1* = 1.2536 -# y_loc_1[1]* = 1.1196 (y_1*) -# y_loc_2[1]* = 1.2536 (x_2*) -# y_loc_2[2]* = 1.2536 (x_3*) From 9ab95b75e958de56e03088e4869df8592200c43e Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 15 Oct 2021 14:23:48 -0400 Subject: [PATCH 25/30] Deleted non-required file --- src/backwardPass0.jl | 562 ------------------------------------------- 1 file changed, 562 deletions(-) delete mode 100644 src/backwardPass0.jl diff --git a/src/backwardPass0.jl b/src/backwardPass0.jl deleted file mode 100644 index 30751091..00000000 --- a/src/backwardPass0.jl +++ /dev/null @@ -1,562 +0,0 @@ -# The functions -# > "solve_all_children", -# > "solve_subproblem_backward", -# > "get_dual_variables_backward", -# > "calculate_bound", -# > "solve_first_stage_problem" -# are derived from similar named functions (backward_pass, -# solve_all_children, solve_subproblem, get_dual_variables, calculate_bound, -# solve_first_stage_problem) in the 'SDDP.jl' package by -# Oscar Dowson and released under the Mozilla Public License 2.0. -# The reproduced function and other functions in this file are also released -# under Mozilla Public License 2.0 - -# Copyright (c) 2021 Christian Fuellner -# Copyright (c) 2021 Oscar Dowson - -# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -################################################################################ - -""" -Executing the backward pass of a loop of DynamicSDDiP. -""" -function backward_pass( - model::SDDP.PolicyGraph{T}, - options::DynamicSDDiP.Options, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers, - scenario_path::Vector{Tuple{T,NoiseType}}, - sampled_states::Vector{Dict{Symbol,Float64}}, - objective_states::Vector{NTuple{N,Float64}}, - belief_states::Vector{Tuple{Int,Dict{T,Float64}}}) where {T,NoiseType,N} - - #################################################################### - # INITIALIZATION - #################################################################### - - # storage for cuts - cuts = Dict{T,Vector{Any}}(index => Any[] for index in keys(model.nodes)) - - # storage for data on solving Lagrangian dual - model.ext[:lag_iterations] = Int[] - model.ext[:lag_status] = Symbol[] - - ############################################################################ - # Traverse backwards through the stages - ############################################################################ - for index in length(scenario_path):-1:1 - outgoing_state = sampled_states[index] - items = BackwardPassItems(T, SDDP.Noise) - - node_index, _ = scenario_path[index] - node = model[node_index] - if length(node.children) == 0 - continue - end - - # Dict to store values of binary approximation of the state - # Note that we could also retrieve this from the actual trial point - # (outgoing_state) or from its approximation via binexpand. However, - # this collection is not only important to obtain the correct values, - # but also to store them together with the symbol/name of the variable. - node.ext[:binary_state_values] = Dict{Symbol, Float64}() - - #################################################################### - # SOLVE ALL CHILDREN PROBLEMS - #################################################################### - solve_all_children( - model, - node, - node_index, - items, - 1.0, - belief_state, - objective_state, - outgoing_state, - algo_params.backward_sampling_scheme, - scenario_path[1:index], - algo_params, - applied_solvers - ) - - # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS - #################################################################### - anchor_points = Dict{Symbol,Float64}() - for (name, value) in outgoing_state - state_comp = node.states[name] - epsilon = algo_params.binary_precision[name] - (approx_state_value, ) = determine_anchor_states(state_comp, value, epsilon) - anchor_points[name] = approx_state_value - end - - @infiltrate algo_params.infiltrate_state in [:all] - - # REFINE BELLMAN FUNCTION BY ADDING CUTS - #################################################################### - TimerOutputs.@timeit DynamicSDDiP_TIMER "update_bellman" begin - new_cuts = refine_bellman_function( - model, - node, - node_index, - node.bellman_function, - options.risk_measures[node_index], - outgoing_state, - anchor_points, - items.bin_state, - items.duals, - items.supports, - items.probability, - items.objectives, - algo_params, - applied_solvers - ) - end - push!(cuts[node_index], new_cuts) - #NOTE: Has to be adapted for stochastic case - push!(model.ext[:lag_iterations], sum(items.lag_iterations)) - push!(model.ext[:lag_status], items.lag_status[1]) - - #TODO: Implement cut-sharing as in SDDP - - end - return cuts -end - - -""" -Solving all children within the backward pass. -""" -function solve_all_children( - model::SDDP.PolicyGraph{T}, - node::SDDP.Node{T}, - node_index::Int64, - items::BackwardPassItems, - belief::Float64, - belief_state, - objective_state, - outgoing_state::Dict{Symbol,Float64}, - backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme, - scenario_path, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers -) where {T} - length_scenario_path = length(scenario_path) - for child in node.children - if isapprox(child.probability, 0.0, atol = 1e-6) - continue - end - child_node = model[child.term] - for noise in SDDP.sample_backward_noise_terms(backward_sampling_scheme, child_node) - if length(scenario_path) == length_scenario_path - push!(scenario_path, (child.term, noise.term)) - else - scenario_path[end] = (child.term, noise.term) - end - #################################################################### - # IF SOLUTIONS FOR THIS NODE ARE CACHED ALREADY, USE THEM - #################################################################### - if haskey(items.cached_solutions, (child.term, noise.term)) - sol_index = items.cached_solutions[(child.term, noise.term)] - push!(items.duals, items.duals[sol_index]) - push!(items.supports, items.supports[sol_index]) - push!(items.nodes, child_node.index) - push!(items.probability, items.probability[sol_index]) - push!(items.objectives, items.objectives[sol_index]) - push!(items.belief, belief) - push!(items.bin_state, items.bin_state[sol_index]) - push!(items.lag_iterations, items.lag_iterations[sol_index]) - push!(items.lag_status, items.lag_status[sol_index]) - else - ################################################################ - # SOLVE THE BACKWARD PASS PROBLEM - ################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_BP" begin - subproblem_results = solve_subproblem_backward( - model, - child_node, - node_index+1, - outgoing_state, - noise.term, - scenario_path, - algo_params, - applied_solvers - ) - end - push!(items.duals, subproblem_results.duals) - push!(items.supports, noise) - push!(items.nodes, child_node.index) - push!(items.probability, child.probability * noise.probability) - push!(items.objectives, subproblem_results.objective) - push!(items.belief, belief) - push!(items.bin_state, subproblem_results.bin_state) - push!(items.lag_iterations, subproblem_results.iterations) - push!(items.lag_status, subproblem_results.lag_status) - items.cached_solutions[(child.term, noise.term)] = length(items.duals) - end - end - end - if length(scenario_path) == length_scenario_path - # No-op. There weren't any children to solve. - else - # Drop the last element (i.e., the one we added). - pop!(scenario_path) - end -end - - -""" -Solving the backward pass problem for one specific child -""" -# Internal function: solve the subproblem associated with node given the -# incoming state variables state and realization of the stagewise-independent -# noise term noise. If require_duals=true, also return the dual variables -# associated with the fixed constraint of the incoming state variables. -function solve_subproblem_backward( - model::SDDP.PolicyGraph{T}, - node::SDDP.Node{T}, - node_index::Int64, - state::Dict{Symbol,Float64}, - noise, - scenario_path::Vector{Tuple{T,S}}, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers; -) where {T,S} - - ############################################################################ - # MODEL PARAMETRIZATION - ############################################################################ - subproblem = node.subproblem - - # Storage for backward pass data - node.ext[:backward_data] = Dict{Symbol,Any}() - - # Parameterize the model. Fix the value of the incoming state variables. - # Then parameterize the model depending on `noise` and set the objective. - set_incoming_state!(node, state) - parameterize(node, noise) - - @infiltrate algo_params.infiltrate_state in [:all] - - ############################################################################ - # CHANGE STATE SPACE IF BINARY APPROXIMATION IS USED - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin - changeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) - end - - ############################################################################ - # INITIALIZE DUALS - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "dual_initialization" begin - dual_vars_initial = initialize_duals(node, subproblem, algo_params.dual_initialization_method) - end - - # RESET SOLVER (as it may have been changed in between for some reason) - ######################################################################## - DynamicSDDiP.set_solver!(node.subproblem, algo_params, applied_solvers, :backward_pass) - - # GET PRIMAL SOLUTION TO BOUND LAGRANGIAN DUAL - ############################################################################ - @infiltrate algo_params.infiltrate_state in [:all] - - # REGULARIZATION - if algo_params.regularization - node.ext[:regularization_data] = Dict{Symbol,Any}() - regularize_backward!(node, subproblem, algo_params.sigma[node_index]) - end - - # SOLVE PRIMAL PROBLEM (REGULARIZED OR NOT) - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_primal" begin - JuMP.optimize!(subproblem) - end - - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - - # NOTE: TO-DO: Attempt numerical recovery as in SDDP - - solver_obj = JuMP.objective_value(subproblem) - @assert JuMP.termination_status(subproblem) == MOI.OPTIMAL - - @infiltrate algo_params.infiltrate_state in [:all] - - # PREPARE ACTUAL BACKWARD PASS METHOD BY DEREGULARIZATION - ############################################################################ - if algo_params.regularization - deregularize_backward!(node, subproblem) - end - - # DUAL SOLUTION - ############################################################################ - # Solve dual and return a dict with the multiplier of the copy constraints. - TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_lagrange" begin - lagrangian_results = get_dual_variables_backward(node, node_index, solver_obj, algo_params, applied_solvers, dual_vars_initial) - end - - dual_values = lagrangian_results.dual_values - bin_state = lagrangian_results.bin_state - objective = lagrangian_results.intercept - iterations = lagrangian_results.iterations - lag_status = lagrangian_results.lag_status - - @infiltrate algo_params.infiltrate_state in [:all] - - ############################################################################ - # REGAIN ORIGINAL MODEL - ############################################################################ - TimerOutputs.@timeit DynamicSDDiP_TIMER "space_change" begin - rechangeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) - end - - return ( - duals = dual_values, - bin_state = bin_state, - objective = objective, - iterations = iterations, - lag_status = lag_status, - ) -end - - -""" -Calling the Lagrangian dual solution method and determining dual variables -required to construct a cut -""" -function get_dual_variables_backward( - node::SDDP.Node, - node_index::Int64, - solver_obj::Float64, - algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers, - dual_vars_initial::Vector{Float64} - ) - - # storages for return of dual values and binary state values (trial point) - dual_values = Dict{Symbol,Float64}() - bin_state = Dict{Symbol, BinaryState}() - - # TODO: implement smart choice for initial duals - number_of_states = length(node.ext[:backward_data][:bin_states]) - # dual_vars = zeros(number_of_states) - #solver_obj = JuMP.objective_value(node.ext[:linSubproblem]) - dual_vars = dual_vars_initial - - lag_obj = 0 - lag_iterations = 0 - lag_status = :none - - # Create an SDDiP integrality_handler here to store the Lagrangian dual information - #TODO: Store tolerances in algo_params - integrality_handler = SDDP.SDDiP(iteration_limit = algo_params.lagrangian_iteration_limit, atol = algo_params.lagrangian_atol, rtol = algo_params.lagrangian_rtol) - integrality_handler = SDDP.update_integrality_handler!(integrality_handler, applied_solvers.MILP, number_of_states) - node.ext[:lagrange] = integrality_handler - - # DETERMINE AND ADD BOUNDS FOR DUAL VARIABLES - ############################################################################ - #TODO: Determine a norm of B (coefficient matrix of binary expansion) - # We use the column sum norm here - # But instead of calculating it exactly, we can also use the maximum - # upper bound of all state variables as a bound - - B_norm_bound = 0 - for (name, state_comp) in node.states - if state_comp.info.in.upper_bound > B_norm_bound - B_norm_bound = state_comp.info.in.upper_bound - end - end - dual_bound = algo_params.sigma[node_index] * B_norm_bound - - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - #|| node.ext[:linSubproblem].ext[:sddp_policy_graph].ext[:iteration] == 12 - - try - # SOLUTION WITHOUT BOUNDED DUAL VARIABLES (BETTER TO OBTAIN BASIC SOLUTIONS) - ######################################################################## - if algo_params.lagrangian_method == :kelley - results = _kelley(node, node_index, solver_obj, dual_vars, integrality_handler, algo_params, applied_solvers, nothing) - lag_obj = results.lag_obj - lag_iterations = results.iterations - lag_status = results.lag_status - elseif algo_params.lagrangian_method == :bundle_level - results = _bundle_level(node, node_index, solver_obj, dual_vars, integrality_handler, algo_params, applied_solvers, nothing) - lag_obj = results.lag_obj - lag_iterations = results.iterations - lag_status = results.lag_status - end - - # OPTIMAL VALUE CHECKS - ######################################################################## - if algo_params.lag_status_regime == :rigorous - if lag_status == :conv - error("Lagrangian dual converged to value < solver_obj.") - elseif lag_status == :sub - error("Lagrangian dual had subgradients zero without LB=UB.") - elseif lag_status == :iter - error("Solving Lagrangian dual exceeded iteration limit.") - end - - elseif algo_params.lag_status_regime == :lax - # all cuts will be used as they are valid even though not necessarily tight - end - - # DUAL VARIABLE BOUND CHECK - ######################################################################## - # if one of the dual variables exceeds the bounds (e.g. in case of an - # discontinuous value function), use bounded version of Kelley's method - bound_check = true - for dual_var in dual_vars - if dual_var > dual_bound - bound_check = false - end - end - - @infiltrate algo_params.infiltrate_state in [:all, :lagrange] - - catch e - SDDP.write_subproblem_to_file(node, "subproblem.mof.json", throw_error = false) - rethrow(e) - end - - # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN - ############################################################################ - for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) - # TODO (maybe) change dual signs inside kelley to match LP duals - dual_values[name] = -dual_vars[i] - - value = integrality_handler.old_rhs[i] - x_name = node.ext[:backward_data][:bin_x_names][name] - k = node.ext[:backward_data][:bin_k][name] - bin_state[name] = BinaryState(value, x_name, k) - end - - return ( - dual_values=dual_values, - bin_state=bin_state, - intercept=lag_obj, - iterations=lag_iterations, - lag_status=lag_status, - ) -end - - -""" -Calculate the lower bound (if minimizing, otherwise upper bound) of the problem -model at the point state. -""" -function calculate_bound( - model::SDDP.PolicyGraph{T}, - root_state::Dict{Symbol,Float64} = model.initial_root_state; - risk_measure = SDDP.Expectation(), -) where {T} - - # Note that here all children of the root node are solved, since the root - # node is not node 1, but node 0. - # In our case, this means that only stage 1 problem is solved again, - # using the updated Bellman function from the backward pass. - - # Initialization. - noise_supports = Any[] - probabilities = Float64[] - objectives = Float64[] - problem_size = Dict{Symbol,Int64}[] - current_belief = SDDP.initialize_belief(model) - - # Solve all problems that are children of the root node. - for child in model.root_children - if isapprox(child.probability, 0.0, atol = 1e-6) - continue - end - node = model[child.term] - for noise in node.noise_terms - subproblem_results = solve_first_stage_problem( - model, - node, - root_state, - noise.term, - Tuple{T,Any}[(child.term, noise.term)], - ) - push!(objectives, subproblem_results.objective) - push!(probabilities, child.probability * noise.probability) - push!(noise_supports, noise.term) - push!(problem_size, subproblem_results.problem_size) - end - end - # Now compute the risk-adjusted probability measure: - risk_adjusted_probability = similar(probabilities) - offset = SDDP.adjust_probability( - risk_measure, - risk_adjusted_probability, - probabilities, - noise_supports, - objectives, - model.objective_sense == MOI.MIN_SENSE, - ) - # Finally, calculate the risk-adjusted value. - return (bound = sum(obj * prob for (obj, prob) in zip(objectives, risk_adjusted_probability)) + - offset, problem_size = problem_size) -end - - -""" -Solving the first-stage problem to determine a lower bound -""" -function solve_first_stage_problem( - model::SDDP.PolicyGraph{T}, - node::SDDP.Node{T}, - state::Dict{Symbol,Float64}, - noise, - scenario_path::Vector{Tuple{T,S}}; -) where {T,S} - - ############################################################################ - # MODEL PARAMETRIZATION - ############################################################################ - subproblem = node.subproblem - - # Parameterize the model. First, fix the value of the incoming state - # variables. Then parameterize the model depending on `noise`. Finally, - # set the objective. - set_incoming_state!(node, state) - parameterize(node, noise) - - ############################################################################ - # SOLUTION - ############################################################################ - JuMP.optimize!(subproblem) - - if haskey(model.ext, :total_solves) - model.ext[:total_solves] += 1 - else - model.ext[:total_solves] = 1 - end - - # Maybe attempt numerical recovery as in SDDP - - state = get_outgoing_state(node) - stage_objective = JuMP.value(node.stage_objective) - objective = JuMP.objective_value(subproblem) - dual_values = get_dual_variables(node, node.integrality_handler) - - ############################################################################ - # DETERMINE THE PROBLEM SIZE - ############################################################################ - problem_size = Dict{Symbol,Int64}() - problem_size[:total_var] = size(JuMP.all_variables(subproblem),1) - problem_size[:bin_var] = JuMP.num_constraints(subproblem, VariableRef, MOI.ZeroOne) - problem_size[:int_var] = JuMP.num_constraints(subproblem, VariableRef, MOI.Integer) - problem_size[:total_con] = JuMP.num_constraints(subproblem, GenericAffExpr{Float64,VariableRef}, MOI.LessThan{Float64}) - + JuMP.num_constraints(subproblem, GenericAffExpr{Float64,VariableRef}, MOI.GreaterThan{Float64}) - + JuMP.num_constraints(subproblem, GenericAffExpr{Float64,VariableRef}, MOI.EqualTo{Float64}) - - return ( - state = state, - duals = dual_values, - objective = objective, - stage_objective = stage_objective, - problem_size = problem_size - ) -end From d7f2a2ba776b548b4927bf933734b597c3a0e4ad Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 15 Oct 2021 14:53:13 -0400 Subject: [PATCH 26/30] Changed state.jl quite a lot to make sure that we can restore bounds and integer type after a variable has been unfixed. --- examples/newExample_1.jl | 145 +++++++++++++++++++++ src/JuMP.jl | 47 ++++--- src/algorithmMain.jl | 89 +++++++++---- src/backwardPass.jl | 5 +- src/bellman.jl | 14 ++- src/binarization.jl | 43 ++++--- src/binaryRefinement.jl | 7 +- src/cutSelection.jl | 12 +- src/duals.jl | 7 +- src/forwardPass.jl | 7 +- src/lagrange.jl | 4 +- src/regularizations.jl | 14 ++- src/solverHandling.jl | 6 +- src/state.jl | 265 +++++++++++++++++++++++++++++---------- src/typedefs.jl | 16 ++- 15 files changed, 505 insertions(+), 176 deletions(-) create mode 100644 examples/newExample_1.jl diff --git a/examples/newExample_1.jl b/examples/newExample_1.jl new file mode 100644 index 00000000..87860ec9 --- /dev/null +++ b/examples/newExample_1.jl @@ -0,0 +1,145 @@ +# Copyright (c) 2021 Christian Fuellner + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +using JuMP +using SDDP +using DynamicSDDiP +using Revise +using Gurobi +using GAMS +#using SCIP +using Infiltrator + + +function model_config() + + # Stopping rules to be used + stopping_rules = [DynamicSDDiP.DeterministicStopping()] + + # Duality / Cut computation configuration + dual_initialization_regime = DynamicSDDiP.ZeroDuals() + dual_solution_regime = DynamicSDDiP.Kelley() + dual_bound_regime = DynamicSDDiP.BothBounds() + dual_status_regime = DynamicSDDiP.Rigorous() + dual_choice_regime = DynamicSDDiP.StandardChoice() + duality_regime = DynamicSDDiP.LagrangianDuality( + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, + dual_initialization_regime = dual_initialization_regime, + dual_bound_regime = dual_bound_regime, + dual_solution_regime = dual_solution_regime, + dual_choice_regime = dual_choice_regime, + dual_status_regime = dual_status_regime, + ) + + # State approximation and cut projection configuration + cut_projection_regime = DynamicSDDiP.BigM() + binary_precision = Dict{Symbol, Float64}() # TODO + + # for (name, state_comp) in model.nodes[1].ext[:lin_states] + # ub = JuMP.upper_bound(state_comp.out) + # + # string_name = string(name) + # if occursin("gen", string_name) + # binaryPrecision[name] = binaryPrecisionFactor * ub + # else + # binaryPrecision[name] = 1 + # end + # end + + state_approximation_regime = DynamicSDDiP.BinaryApproximation( + binary_precision = binary_precision, + cut_projection_regime = cut_projection_regime) + + # Regularization configuration + regularization_regime = DynamicSDDiP.Regularization(sigma = [0.0, 1.0], sigma_factor = 5.0) + + # Cut selection configuration + cut_selection_regime = DynamicSDDiP.NoCutSelection() + + # File for logging + log_file = "C:/Users/cg4102/Documents/julia_logs/newExample_1.log" + + # Infiltration for debugging + infiltrate_state = :none + + # Definition of algo_params + algo_params = DynamicSDDiP.AlgoParams( + stopping_rules = stopping_rules, + state_approximation_regime = state_approximation_regime, + regularization_regime = regularization_regime, + duality_regime = duality_regime, + cut_selection_regime = cut_selection_regime, + log_file = log_file, + infiltrate_state = infiltrate_state, + ) + + # Define solvers to be used + applied_solvers = DynamicSDDiP.AppliedSolvers( + LP = "Gurobi", + MILP = "Gurobi", + MINLP = "Gurobi", + NLP = "SCIP", + Lagrange = "Gurobi", + ) + + # Start model with used configuration + model_starter( + algo_params, + applied_solvers, + ) +end + + +function model_starter( + algo_params::DynamicSDDiP.AlgoParams = DynamicSDDiP.AlgoParams(), + applied_solvers::DynamicSDDiP.AppliedSolvers = DynamicSDDiP.AppliedSolvers(), + ) + + ############################################################################ + # DEFINE MODEL + ############################################################################ + model = model_definition() + + ############################################################################ + # SOLVE MODEL + ############################################################################ + DynamicSDDiP.solve(model, algo_params, applied_solvers) +end + + +function model_definition() + + number_of_stages = 2 + + model = SDDP.LinearPolicyGraph( + stages = number_of_stages, + lower_bound = 0.0, + optimizer = GAMS.Optimizer, + sense = :Min + ) do subproblem, t + + ######################################################################## + # DEFINE STAGE-t MODEL + ######################################################################## + + # State variables + JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, Bin, initial_value = 0) + + # Constraints + b = subproblem[:b] + JuMP.@constraint(subproblem, b.out == 1.2 + b.in) + + # Stage objective + SDDP.@stageobjective(subproblem, 1) + + end + + return model +end + +model_config() diff --git a/src/JuMP.jl b/src/JuMP.jl index cf553264..b46cacac 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -13,7 +13,7 @@ function JuMP.build_variable( _error::Function, info::JuMP.VariableInfo, - ::Type{DynamicSDDiP.State}; + ::Type{SDDP.State}; initial_value = NaN, kwargs..., ) @@ -25,7 +25,7 @@ function JuMP.build_variable( " the root node.", ) end - return StateInfo( + return SDDP.StateInfo( JuMP.VariableInfo( false, NaN, # lower bound @@ -44,37 +44,34 @@ function JuMP.build_variable( ) end -function JuMP.add_variable(subproblem::JuMP.Model, state_info::StateInfo, name::String) - - # Store bounds also in state, since they have to be relaxed and readded later - # if state_info.out.has_lb - # lb = state_info.out.lower_bound - # else - # lb = -Inf - # end - # - # if state_info.out.has_ub - # ub = state_info.out.upper_bound - # else - # ub = Inf - # end - - state = State( - JuMP.add_variable(problem, JuMP.ScalarVariable(state_info.in), name * "_in"), - JuMP.add_variable(problem, JuMP.ScalarVariable(state_info.out), name * "_out"), - state_info +function JuMP.add_variable( + subproblem::JuMP.Model, + state_info::SDDP.StateInfo, + name::String, +) + state = SDDP.State( + JuMP.add_variable( + subproblem, + JuMP.ScalarVariable(state_info.in), + name * "_in", + ), + JuMP.add_variable( + subproblem, + JuMP.ScalarVariable(state_info.out), + name * "_out", + ), ) - - node = get_node(subproblem) + node = SDDP.get_node(subproblem) sym_name = Symbol(name) @assert !haskey(node.states, sym_name) # JuMP prevents duplicate names. node.states[sym_name] = state - graph = get_policy_graph(subproblem) + graph = SDDP.get_policy_graph(subproblem) graph.initial_root_state[sym_name] = state_info.initial_value return state end -JuMP.variable_type(model::JuMP.Model, ::Type{State}) = State + +JuMP.variable_type(model::JuMP.Model, ::Type{State}) = SDDP.State function JuMP.value(state::State{JuMP.VariableRef}) return State(JuMP.value(state.in), JuMP.value(state.out)) diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index ad5ca2c7..b3a3f8b5 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -15,6 +15,8 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ +const DynamicSDDiP_TIMER = TimerOutputs.TimerOutput() + """ Solves the `model`. In contrast to SDDP.jl, all parameters configuring the algorithm are given (and possibly pre-defined) in algo_params. @@ -35,11 +37,11 @@ function solve( log_file_handle = open(algo_params.log_file, "a") log = Log[] - if print_level > 0 + if algo_params.print_level > 0 print_helper(print_banner, log_file_handle) end - if print_level > 1 + if algo_params.print_level > 1 print_helper(print_parameters, log_file_handle, algo_params, applied_solvers) end @@ -76,6 +78,20 @@ function solve( end end + # Prepare sigma + #--------------------------------------------------------------------------- + regime = algo_params.regularization_regime + if regime == DynamicSDDiP.Regularization && isempty(regime.sigma) + for (node_index, _) in model.nodes + if node_index == 1 + # first stage requires no regularization + push!(regime.sigma, 0.0) + else + push!(regime.sigma, 1.0) + end + end + end + # Prepare options for logging #--------------------------------------------------------------------------- options = DynamicSDDiP.Options( @@ -91,7 +107,8 @@ function solve( ############################################################################ # Update the nodes with the selected cut type (SINGLE_CUT or MULTI_CUT) # and the cut deletion minimum. - if algo_params.cut_selection_regime.cut_deletion_minimum < 0 + regime = algo_params.cut_selection_regime + if regime == DynamicSDDiP.CutSelection && regime.cut_deletion_minimum < 0 algo_params.cut_selection_regime.cut_deletion_minimum = typemax(Int) end @@ -99,12 +116,28 @@ function solve( #--------------------------------------------------------------------------- # fortunately, node.bellman_function requires no specific type for (key, node) in model.nodes - node.bellman_function = DynamicSDDiP.initialize_bellman_function(bellman_function, model, node) - node.bellman_function.cut_type = algo_params.cut_type - node.bellman_function.global_theta.cut_oracle.deletion_minimum = - algo_params.cut_selection_regime.cut_deletion_minimum - for oracle in node.bellman_function.local_thetas - oracle.cut_oracle.deletion_minimum = algo_params.cut_selection_regime.cut_deletion_minimum + + if key != model.root_node + + if model.objective_sense == MOI.MIN_SENSE + lower_bound = JuMP.lower_bound(node.bellman_function.global_theta.theta) + upper_bound = Inf + elseif model.objective_sense == MOI.MAX_SENSE + upper_bound = JuMP.upper_bound(node.bellman_function.global_theta.theta) + lower_bound = -Inf + end + + bellman_function = BellmanFunction(lower_bound = lower_bound, upper_bound = upper_bound) + node.bellman_function = DynamicSDDiP.initialize_bellman_function(bellman_function, model, node) + node.bellman_function.cut_type = algo_params.cut_type + + if algo_params.cut_selection_regime == DynamicSDDiP.CutSelection + node.bellman_function.global_theta.cut_oracle.deletion_minimum = + algo_params.cut_selection_regime.cut_deletion_minimum + for oracle in node.bellman_function.local_thetas + oracle.cut_oracle.deletion_minimum = algo_params.cut_selection_regime.cut_deletion_minimum + end + end end end @@ -113,11 +146,11 @@ function solve( ############################################################################ status = :not_solved try - status = solve_DynamicSDDiP(parallel_scheme, model, options, algo_params, applied_solvers) + status = solve_DynamicSDDiP(algo_params.parallel_scheme, model, options, algo_params, applied_solvers) catch ex if isa(ex, InterruptException) status = :interrupted - interrupt(parallel_scheme) + interrupt(algo_params.parallel_scheme) else close(log_file_handle) rethrow(ex) @@ -159,25 +192,26 @@ function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGrap #----------------------------------------------------------------------- if node_index > 1 for (i, (name, state)) in enumerate(node.states) - # Get correct state_info - state_info = model.nodes[node_index-1].states[name].info.out + state_out_previous_stage = model.nodes[node_index-1].states[name].out + state_in = state.in - if state_info.has_lb - JuMP.set_lower_bound(state.in, state_info.lower_bound) - end - if state_info.has_ub - JuMP.set_upper_bound(state.in, state_info.upper_bound) - end - if state_info.binary - JuMP.set_binary(state.in) - elseif state_info.integer - JuMP.set_integer(state.in) - end + set_up_state_in_info!(state_out_previous_stage, state_in) - # Store info to reset it later - state.info.in = state_info end end + + # Store info for all states (state.in, state.out) for later + # This is required for variable fixing and unfixing + #----------------------------------------------------------------------- + node.ext[:state_info_storage] = Dict{Symbol,DynamicSDDiP.StateInfoStorage}() + + for (i, (name, state)) in enumerate(node.states) + variable_info_in = get_variable_info(state.in) + variable_info_out = get_variable_info(state.out) + + node.ext[:state_info_storage][name] = DynamicSDDiP.StateInfoStorage(variable_info_in, variable_info_out) + end + end @infiltrate algo_params.infiltrate_state == :all @@ -186,7 +220,8 @@ function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGrap # CALL ACTUAL SOLUTION PROCEDURE ############################################################################ TimerOutputs.@timeit DynamicSDDiP_TIMER "loop" begin - status = master_loop(parallel_scheme, model, options, algo_params, applied_solvers) + status = master_loop(parallel_scheme, model, options, algo_params, + applied_solvers, algo_params.regularization_regime) end return status diff --git a/src/backwardPass.jl b/src/backwardPass.jl index 0de6f6f9..cec3d998 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -82,7 +82,8 @@ function backward_pass( ######################################################################## # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS ######################################################################## - anchor_states = determine_anchor_states(node, outgoing_state, algo_params.state_approximation_regime) + variable_info = node.ext[:state_info_storage][name].out + anchor_states = determine_anchor_states(node, outgoing_state, algo_params.state_approximation_regime, variable_info) @infiltrate algo_params.infiltrate_state in [:all] ######################################################################## @@ -121,7 +122,7 @@ function backward_pass( #TODO: Implement cut-sharing as in SDDP end - + return cuts end diff --git a/src/bellman.jl b/src/bellman.jl index 61cb5cfa..c8bd00fd 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -419,22 +419,24 @@ function add_cut_constraints_to_models( they are required anyway. """ - if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub || !state_comp.info.out.binary + variable_info = node.ext[:state_info_storage][state_name].out + + if !isfinite(variable_info.upper_bound) || !variable_info.has_ub || !variable_info.binary error("When using DynamicSDDiP, state variables require an upper bound.") end #################################################################### # DETERMINE K (number of [0,1] variables required) AND BETA #################################################################### - if state_comp.info.out.binary + if variable_info.binary beta = 1 - K = SDDP._bitsrequired(state_comp.info.out.upper_bound) - elseif state_comp.info.out.integer + K = SDDP._bitsrequired(variable_info.upper_bound) + elseif variable_info.integer beta = 1 - K = SDDP._bitsrequired(state_comp.info.out.upper_bound) + K = SDDP._bitsrequired(variable_info.upper_bound) else beta = cut.binary_precision[name] - K = SDDP._bitsrequired(round(Int, state_comp.info.out.upper_bound / beta)) + K = SDDP._bitsrequired(round(Int, variable_info.upper_bound / beta)) end #################################################################### diff --git a/src/binarization.jl b/src/binarization.jl index 8e9e85f1..eced5283 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -53,8 +53,11 @@ function changeStateSpace!( bw_data[:fixed_state_value][state_name] = fixed_value beta = binary_precision[state_name] + # Get variable info for this state to restore bounds and integer type + variable_info = node.ext[:state_info_storage][name].in + # Set up state for backward pass using binary approximation - setup_state_binarization!(subproblem, state_comp, state_name, beta, bw_data) + setup_state_binarization!(subproblem, state_comp, state_name, beta, bw_data, variable_info) end return @@ -151,10 +154,11 @@ Setting up the binary state variables. """ function setup_state_binarization!( subproblem::JuMP.Model, - state_comp::State, + state_comp::SDDP.State, state_name::Symbol, binary_precision::Float64, - bw_data::Dict{Symbol,Any} + bw_data::Dict{Symbol,Any}, + variable_info::DynamicSDDiP.VariableInfo, ) # Get name of state variable in String representation @@ -163,7 +167,7 @@ function setup_state_binarization!( ############################################################################ # STATE IS ALREADY BINARY ############################################################################ - if state_comp.info.in.binary + if variable_info.binary """ In this case, the variable must not be unfixed and, in principle, no new variables or constraints have to be introduced. @@ -209,14 +213,14 @@ function setup_state_binarization!( ######################################################################## # Unfix the original state JuMP.unfix(state_comp.in) - follow_state_unfixing!(state_comp) + follow_state_unfixing!(state_comp, variable_info) else - if !isfinite(state_comp.info.in.upper_bound) || !state_comp.info.in.has_ub + if !isfinite(variable_info.upper_bound) || !variable_info.has_ub error("When using SDDiP, state variables require an upper bound.") end - if state_comp.info.in.integer + if variable_info.integer #################################################################### # STATE VARIABLE IS INTEGER #################################################################### @@ -229,7 +233,7 @@ function setup_state_binarization!( #################################################################### # INTRODUCE BINARY VARIABLES TO THE PROBLEM #################################################################### - num_vars = SDDP._bitsrequired(state_comp.info.in.upper_bound) + num_vars = SDDP._bitsrequired(variable_info.upper_bound) binary_vars = JuMP.@variable( subproblem, @@ -260,7 +264,7 @@ function setup_state_binarization!( # FIX NEW VARIABLES #################################################################### # Get fixed values from fixed value of original state - fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], state_comp.info.in.upper_bound) + fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], variable_info.upper_bound) # Fix binary variables for i in 1:num_vars #JuMP.unset_binary(binary_vars[i].in) @@ -272,7 +276,7 @@ function setup_state_binarization!( #################################################################### # Unfix the original state JuMP.unfix(state_comp.in) - follow_state_unfixing!(state_comp) + follow_state_unfixing!(state_comp, variable_info) else #################################################################### @@ -285,7 +289,7 @@ function setup_state_binarization!( #################################################################### # INTRODUCE BINARY VARIABLES TO THE PROBLEM #################################################################### - num_vars = SDDP._bitsrequired(round(Int, state_comp.info.in.upper_bound / beta)) + num_vars = SDDP._bitsrequired(round(Int, variable_info.upper_bound / beta)) binary_vars = JuMP.@variable( subproblem, @@ -319,7 +323,7 @@ function setup_state_binarization!( # FIX NEW VARIABLES #################################################################### # Get fixed values from fixed value of original state - fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], state_comp.info.in.upper_bound, beta) + fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], variable_info.upper_bound, beta) # Fix binary variables for i in 1:num_vars #JuMP.unset_binary(binary_vars[i].in) @@ -331,7 +335,7 @@ function setup_state_binarization!( #################################################################### # Unfix the original state JuMP.unfix(state_comp.in) - follow_state_unfixing!(state_comp) + follow_state_unfixing!(state_comp, variable_info) end end @@ -365,24 +369,25 @@ end Determining a single anchor state in the original space. """ function determine_anchor_state( - state_comp::State, + state_comp::SDDP.State, state_value::Float64, binaryPrecision::Float64, + variable_info::DynamicSDDiP.VariableInfo, ) - if state_comp.info.out.binary + if variable_info.binary fixed_binary_values = state_value approx_state_value = state_value else - if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub + if !isfinite(variable_info.upper_bound) || !variable_info.has_ub error("When using SDDiP, state variables require an upper bound.") end - if state_comp.info.out.integer - fixed_binary_values = SDDP.binexpand(state_value, state_comp.info.out.upper_bound) + if variable_info.integer + fixed_binary_values = SDDP.binexpand(state_value, variable_info.upper_bound) approx_state_value = SDDP.bincontract(fixed_binary_values) else - fixed_binary_values = SDDP.binexpand(state_value, state_comp.info.out.upper_bound, binaryPrecision) + fixed_binary_values = SDDP.binexpand(state_value, variable_info.upper_bound, binaryPrecision) approx_state_value = SDDP.bincontract(fixed_binary_values, binaryPrecision) end end diff --git a/src/binaryRefinement.jl b/src/binaryRefinement.jl index d5d2209f..8e44f441 100644 --- a/src/binaryRefinement.jl +++ b/src/binaryRefinement.jl @@ -72,9 +72,12 @@ function binary_refinement( # Consider stage 2 here (should be the same for all following stages) # Precision is only used (and increased) for continuous variables for (name, state_comp) in model.nodes[2].states - if !state_comp.info.in.binary && !state_comp.info.in.integer + + variable_info = node.ext[:state_info_storage][name].in + + if !variable_info.binary && !variable_info.integer current_prec = binary_precision[name] - ub = state_comp.info.in.upper_bound + ub = variable_info.upper_bound K = SDDP._bitsrequired(round(Int, ub / current_prec)) new_prec = ub / sum(2^(k-1) for k in 1:K+1) diff --git a/src/cutSelection.jl b/src/cutSelection.jl index e273acec..c1a5218e 100644 --- a/src/cutSelection.jl +++ b/src/cutSelection.jl @@ -321,7 +321,9 @@ function _eval_height(node::SDDP.Node, cut::DynamicSDDiP.NonlinearCut, binary_variables_so_far = 0 for (i, (state_name, state_comp)) in enumerate(node.states) - if state_comp.info.out.binary + variable_info = node.ext[:state_info_storage][state_name].out + + if variable_info.binary #################################################################### # BINARY CASE #################################################################### @@ -346,17 +348,17 @@ function _eval_height(node::SDDP.Node, cut::DynamicSDDiP.NonlinearCut, #################################################################### # NON-BINARY CASE #################################################################### - if !isfinite(state_comp.info.out.upper_bound) || !state_comp.info.out.has_ub + if !isfinite(variable_info.upper_bound) || !variable_info.has_ub error("When using SDDiP, state variables require an upper bound.") end # Get K and beta - if state_comp.info.out.integer + if variable_info.integer beta = 1 - K = SDDP._bitsrequired(state_comp.info.out.upper_bound) + K = SDDP._bitsrequired(variable_info.upper_bound) else beta = cut.binary_precision[state_name] - K = SDDP._bitsrequired(round(Int, state_comp.info.out.upper_bound / beta)) + K = SDDP._bitsrequired(round(Int, variable_info.upper_bound / beta)) end # introduce binary variables to the model diff --git a/src/duals.jl b/src/duals.jl index 1825b21a..81cedd30 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -376,8 +376,11 @@ function get_norm_bound( B_norm_bound = 0 for (name, state_comp) in node.states - if state_comp.info.in.upper_bound > B_norm_bound - B_norm_bound = state_comp.info.in.upper_bound + + variable_info = node.ext[:state_info_storage][name].in + + if variable_info.upper_bound > B_norm_bound + B_norm_bound = variable_info.upper_bound end end dual_bound = algo_params.regularization_regime.sigma[node_index] * B_norm_bound diff --git a/src/forwardPass.jl b/src/forwardPass.jl index bafaee81..ae30686d 100644 --- a/src/forwardPass.jl +++ b/src/forwardPass.jl @@ -59,6 +59,7 @@ function forward_pass(model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, incoming_state_value, # only values, no State struct! noise, scenario_path[1:depth], + algo_params, algo_params.regularization_regime, ) end @@ -97,7 +98,7 @@ function solve_subproblem_forward( state::Dict{Symbol,Float64}, noise, scenario_path::Vector{Tuple{T,S}}, - infiltrate_state::Symbol, + algo_params::DynamicSDDiP.AlgoParams, regularization_regime::DynamicSDDiP.AbstractRegularizationRegime; ) where {T,S} @@ -123,7 +124,7 @@ function solve_subproblem_forward( ############################################################################ # SOLUTION ############################################################################ - @infiltrate infiltrate_state in [:all] + @infiltrate algo_params.infiltrate_state in [:all] JuMP.optimize!(subproblem) # Maybe attempt numerical recovery as in SDDP @@ -131,7 +132,7 @@ function solve_subproblem_forward( state = get_outgoing_state(node) objective = JuMP.objective_value(subproblem) stage_objective = objective - JuMP.value(bellman_term(node.bellman_function)) - @infiltrate infiltrate_state in [:all] + @infiltrate algo_params.infiltrate_state in [:all] ############################################################################ # DE-REGULARIZE SUBPROBLEM IF REQUIRED diff --git a/src/lagrange.jl b/src/lagrange.jl index d18a8b87..ad13da4e 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -320,7 +320,7 @@ function restore_copy_constraints!( ) for (i, (_, bin_state)) in enumerate(node.ext[:backward_data][:bin_states]) - #prepare_state_fixing!(node, state_comp) + # prepare_state_fixing!(node, state_comp) JuMP.fix(bin_state, x_in_value[i], force = true) end @@ -334,7 +334,7 @@ function restore_copy_constraints!( ) for (i, (_, state)) in enumerate(node.states) - #prepare_state_fixing!(node, state_comp) + # prepare_state_fixing!(node, state_comp) JuMP.fix(state.in, x_in_value[i], force = true) end diff --git a/src/regularizations.jl b/src/regularizations.jl index 4b6779fe..74a5fe71 100644 --- a/src/regularizations.jl +++ b/src/regularizations.jl @@ -17,20 +17,20 @@ function regularize_subproblem!(node::SDDP.Node, node_index::Int64, # It is then subtracted from the fixed value to obtain the so called slack. reg_data = node.ext[:regularization_data] - reg_data[:fixed_state_value] = Dict{Symbol,Float64}() reg_data[:slacks] = Any[] reg_data[:reg_variables] = JuMP.VariableRef[] reg_data[:reg_constraints] = JuMP.ConstraintRef[] number_of_states = 0 - # UNFIX THE STATE VARIABLES + # UNFIX THE STATE VARIABLES (RELAXATION) ############################################################################ for (i, (name, state_comp)) in enumerate(node.states) reg_data[:fixed_state_value][name] = JuMP.fix_value(state_comp.in) push!(reg_data[:slacks], reg_data[:fixed_state_value][name] - state_comp.in) JuMP.unfix(state_comp.in) - follow_state_unfixing!(state_comp) + variable_info = node.ext[:state_info_storage][name].in + follow_state_unfixing!(state_comp, variable_info) number_of_states = i end @@ -141,8 +141,12 @@ function regularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Floa ############################################################################ Umax = 0 for (i, (name, state_comp)) in enumerate(node.states) - if state_comp.info.out.upper_bound > Umax - Umax = state_comp.info.out.upper_bound + + # TODO: is .out correct here? + variable_info = node.ext[:state_info_storage][name].out + + if variable_info.upper_bound > Umax + Umax = variable_info.upper_bound end end # Here, not sigma, but a different regularization parameter is used diff --git a/src/solverHandling.jl b/src/solverHandling.jl index da41d666..93ab2e8a 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -49,21 +49,21 @@ function set_solver!( # SET THE CORRECT SOLVER WITH THE REQUIRED PROPERTIES ############################################################################ if solver == "CPLEX" - set_optimizer(node.subproblem, optimizer_with_attributes( + set_optimizer(subproblem, optimizer_with_attributes( GAMS.Optimizer, "Solver"=>solver, "optcr"=>0.0, "numericalemphasis"=>numerical_focus) ) elseif solver == "Gurobi" - set_optimizer(node.subproblem, optimizer_with_attributes( + set_optimizer(subproblem, optimizer_with_attributes( GAMS.Optimizer, "Solver"=>solver, "optcr"=>0.0, "NumericFocus"=>numerical_focus) ) else - set_optimizer(node.subproblem, optimizer_with_attributes( + set_optimizer(subproblem, optimizer_with_attributes( GAMS.Optimizer, "Solver"=>solver, "optcr"=>0.0) diff --git a/src/state.jl b/src/state.jl index 645d764d..cbbc6f6c 100644 --- a/src/state.jl +++ b/src/state.jl @@ -17,43 +17,163 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ -# must be mutable in my case, as in_part has to be set later -mutable struct StateInfo - in::JuMP.VariableInfo - out::JuMP.VariableInfo - initial_value::Float64 - kwargs::Any +""" +This struct is used to store the bound and integer information of a state +variable (either .in or .out). +This is required to be able to retrieve this information after fixing/unfixing +the states for regularization or relaxation purposes. + +In NCNBD.jl this was done by introducing a struct NCNBD.State with a field +stateInfo, however only for the linearized model. Overwriting the SDDP.State in +general without rewriting a lot of SDDP code does not seem possible. +""" +mutable struct VariableInfo + has_lb::Bool + lower_bound::Float64 + has_ub::Bool + upper_bound::Float64 + binary::Bool + integer::Bool + + function VariableInfo( + has_lb = false, + lower_bound = NaN, + has_ub = false, + upper_bound = NaN, + binary = false, + integer = false, + ) + return new( + has_lb, lower_bound, has_ub, upper_bound, binary, integer + ) + end end -struct State{T} - # The incoming state variable. - in::T - # The outgoing state variable. - out::T - # StateInfo - info::StateInfo +""" +This struct is used to store the bound and integer information of a state +variable (both .in or .out) by using VariableInfo structs. +This is required to be able to retrieve this information after fixing/unfixing +the states for regularization or relaxation purposes. +""" +mutable struct StateInfoStorage + in::DynamicSDDiP.VariableInfo + out::DynamicSDDiP.VariableInfo end +""" +Function to set up the storage in VariableInfo. +""" +function get_variable_info(state::VariableRef) -# Internal function: set the incoming state variables of node to the values -# contained in state. -function set_incoming_state!!(node::SDDP.Node, state::Dict{Symbol,Float64}) - for (state_name, value) in state + variable_info = DynamicSDDiP.VariableInfo() - prepare_state_fixing!(node, state_name) + if JuMP.has_lower_bound(state) + variable_info.has_lb = true + variable_info.lower_bound = JuMP.lower_bound(state) + end + if JuMP.has_upper_bound(state) + variable_info.has_ub = true + variable_info.upper_bound = JuMP.upper_bound(state) + end + if JuMP.is_fixed(state) + variable_info.has_fix = true + variable_info.fixed_value = JuMP.fix_value(state) + end + if JuMP.is_binary(state) + variable_info.binary = true + end + if JuMP.is_integer(state) + variable_info.integer = true + end - # Fix value (bounds are automatically deleted by force argument) + return variable_info +end + + +################################################################################ + +""" +Set the incoming state variable of the node to the values contained in the +state dict. This basically means that the variable is fixed. + +This requires to delete the binary or integer type (with function +prepare_state_fixing!()), cause otherwise problems occur when fixing the +variables. The bounds are not removed explicitly, though, since this can +be automatically done via force=true in the fix command. +""" +function set_incoming_state!(node::SDDP.Node, state::Dict{Symbol,Float64}) + for (state_name, value) in state + prepare_state_fixing!(node, state_name) JuMP.fix(node.states[state_name].in, value, force=true) end return end -# Internal function: get the values of the outgoing state variables in node. -# Requires node.subproblem to have been solved with PrimalStatus == -# FeasiblePoint. +""" +Preparation of fixing a state variable with three different types of argument. + +Note that this differentiation is required since we use the term "state" for +different things. One time for the dict of SDDP.States in our model, each +basically containing two variable references. Another time for a current state +(incoming_state, outgoing_state) like the trial point that we are in. This is +a Dict{Symbol,Float64} then. + +The first method with the symbol is used for setting the incoming state, since +there we loop over all states getting the state_name (::Symbol) and the +value (::Float64) from the current state dict (which is the incoming_state_value +dict, actually). + +The second method with the SDDP.State is used for resetting the model after +a regularization (or a Lagrangian relaxation). In this case, we reset the +integer and binary type of the relaxed variables and the bounds and then fix +them again to their originally fixed values. +Note that for the Lagrangian dual (or the binarization) this is not used so far, +since we do not use binary, but continuous [0,1] variables there, and thus +the variables are not integer or binary anyway. + +I'm not sure what the third method is for. + +""" +function prepare_state_fixing!(node::SDDP.Node, state_name::Symbol) + if JuMP.is_binary(node.states[state_name].in) + JuMP.unset_binary(node.states[state_name].in) + elseif JuMP.is_integer(node.states[state_name].in) + JuMP.unset_integer(node.states[state_name].in) + end + return +end + +function prepare_state_fixing!(node::SDDP.Node, state::SDDP.State) + if JuMP.is_binary(state.in) + JuMP.unset_binary(state.in) + elseif JuMP.is_integer(state.in) + JuMP.unset_integer(state.in) + end +end + +# function prepare_state_fixing_binary!(node::SDDP.Node, state::JuMP.VariableRef) +# if JuMP.is_binary(state) +# JuMP.unset_binary(state) +# elseif JuMP.is_integer(state) +# JuMP.unset_integer(state) +# end +# return +# end + +################################################################################ + +""" +Get the outgoing state which can be used on the following stage +to set the incoming state. + +Requires node.subproblem to have been solved with PrimalStatus == FeasiblePoint. + +I'm not sure if I actually need this function outside of NCNBD or if I could +simply use the SDDP one. +""" function get_outgoing_state(node::SDDP.Node) values = Dict{Symbol,Float64}() - for (name, state_comp) in node.ext[:lin_states] + for (name, state_comp) in node.states # To fix some cases of numerical infeasiblities, if the outgoing value # is outside its bounds, project the value back onto the bounds. There # is a pretty large (×5) penalty associated with this check because it @@ -77,56 +197,26 @@ function get_outgoing_state(node::SDDP.Node) return values end -# Delete binary and integer type of state variables, since I once had some -# problems with fixing working properly then. -# May not be required, though. -# Bounds are not reset, since this can be done automatically using -# force=true when fixing. -function prepare_state_fixing!(node::SDDP.Node, state_name::Symbol) - - if JuMP.is_binary(node.states[state_name].in) - JuMP.unset_binary(node.states[state_name].in) - elseif JuMP.is_integer(node.states[state_name].in) - JuMP.unset_integer(node.states[state_name].in) - end - - return -end - -function prepare_state_fixing!(node::SDDP.Node, state::State) - - if JuMP.is_binary(state.in) - JuMP.unset_binary(state.in) - elseif JuMP.is_integer(state.in) - JuMP.unset_integer(state.in) - end -end - -function prepare_state_fixing_binary!(node::SDDP.Node, state::JuMP.VariableRef) - - if JuMP.is_binary(state) - JuMP.unset_binary(state) - elseif JuMP.is_integer(state) - JuMP.unset_integer(state) - end +################################################################################ - return -end +""" +This method is the counterpart to prepare_state_fixing!(). -# Reset binary and integer type of state variables. -# Reset bounds. -# May not be required, though. -function follow_state_unfixing!(state::State) +It makes sure that if a state variable is unfixed (e.g. during the regularization +or binarization process), the bounds and integer type associated with this +state originally are reintroduced. +""" +function follow_state_unfixing!(state::SDDP.State, variable_info::DynamicSDDiP.VariableInfo) - if state.info.in.has_lb - JuMP.set_lower_bound(state.in, state.info.in.lower_bound) + if variable_info.has_lb + JuMP.set_lower_bound(state.in, variable_info.lower_bound) end - if state.info.in.has_ub - JuMP.set_upper_bound(state.in, state.info.in.upper_bound) + if variable_info.has_ub + JuMP.set_upper_bound(state.in, variable_info.in.upper_bound) end - if state.info.in.binary + if variable_info.binary JuMP.set_binary(state.in) - elseif state.info.in.integer + elseif variable_info.integer JuMP.set_integer(state.in) end @@ -141,8 +231,45 @@ function follow_state_unfixing_binary!(state::JuMP.VariableRef) return end +################################################################################ + +""" +Struct to store information on the [0,1] (or binary) variables created +in the backward pass in case of BinaryApproximation. + +value: the value of the original state (which has been unfixed) +x_name: the name of the original state, the BinaryState is associated with +k: the number of components of the [0,1] variable +""" struct BinaryState value::Float64 - x_name::Symbol # name of original state it is related to - k::Int64 # index and exponent + x_name::Symbol + k::Int64 +end + +################################################################################ + +""" +Function to store the bound and integer information of the .out state of the +previous stage in the .in state of the current stage. +This is required as we - in contrast to SDDP.jl - need also bounds for the +incoming states when they are relaxed. +""" +function set_up_state_in_info!(state_out_previous_stage::JuMP.VariableRef, state_in::JuMP.VariableRef) + + if JuMP.has_lower_bound(state_out_previous_stage) + lower_bound = JuMP.lower_bound(state_out_previous_stage) + JuMP.set_lower_bound(state_in, lower_bound) + end + if JuMP.has_upper_bound(state_out_previous_stage) + upper_bound = JuMP.upper_bound(state_out_previous_stage) + JuMP.set_upper_bound(state_in, upper_bound) + end + if JuMP.is_binary(state_out_previous_stage) + JuMP.set_binary(state_in) + elseif JuMP.is_integer(state_out_previous_stage) + JuMP.set_integer(state_in) + end + + return end diff --git a/src/typedefs.jl b/src/typedefs.jl index 399c4cf1..fc9faf66 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -224,9 +224,9 @@ abstract type AbstractRegularizationRegime end mutable struct Regularization <: AbstractRegularizationRegime sigma :: Vector{Float64} sigma_factor :: Float64 - function BinaryApproximation(; - sigma = 1, - sigma_factor = 5 + function Regularization(; + sigma = Float64[], + sigma_factor = 5.0 ) return new(sigma, sigma_factor) end @@ -258,7 +258,7 @@ mutable struct LagrangianDuality <: AbstractDualityRegime dual_solution_regime::AbstractDualSolutionRegime dual_choice_regime::AbstractDualChoiceRegime dual_status_regime::AbstractDualStatusRegime - function BinaryApproximation(; + function LagrangianDuality(; atol = 1e-8, rtol = 1e-8, iteration_limit = 1000, @@ -340,6 +340,7 @@ mutable struct AlgoParams log_frequency::Int log_file::String run_numerical_stability_report::Bool + numerical_focus::Bool infiltrate_state::Symbol function AlgoParams(; @@ -360,14 +361,16 @@ mutable struct AlgoParams log_frequency = 1, log_file = "DynamicSDDiP.log", run_numerical_stability_report = true, + numerical_focus = false, infiltrate_state = :none, - ) + ) return new( stopping_rules, state_approximation_regime, regularization_regime, duality_regime, cut_selection_regime, + risk_measure, forward_pass, sampling_scheme, backward_sampling_scheme, @@ -379,8 +382,9 @@ mutable struct AlgoParams log_frequency, log_file, run_numerical_stability_report, + numerical_focus, infiltrate_state, - ) + ) end end From 08fa12d650067e11731256a705560628a6e0d956 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 15 Oct 2021 16:46:35 -0400 Subject: [PATCH 27/30] A lot of corrections due to runtime errors --- examples/newExample_1.jl | 29 ++--- examples/subproblem.mof.json | 216 +++++++++++++++++++++++++++++++++++ src/JuMP.jl | 125 ++++++++++---------- src/algorithmMain.jl | 10 +- src/backwardPass.jl | 20 ++-- src/binarization.jl | 2 +- src/duals.jl | 10 +- src/forwardPass.jl | 4 +- src/lagrange.jl | 6 +- src/objective.jl | 16 +-- src/regularizations.jl | 7 +- src/sigmaTest.jl | 4 +- src/solverHandling.jl | 2 + src/state.jl | 21 ++-- stuff.jl | 15 ++- 15 files changed, 357 insertions(+), 130 deletions(-) create mode 100644 examples/subproblem.mof.json diff --git a/examples/newExample_1.jl b/examples/newExample_1.jl index 87860ec9..39496ebc 100644 --- a/examples/newExample_1.jl +++ b/examples/newExample_1.jl @@ -38,18 +38,7 @@ function model_config() # State approximation and cut projection configuration cut_projection_regime = DynamicSDDiP.BigM() - binary_precision = Dict{Symbol, Float64}() # TODO - - # for (name, state_comp) in model.nodes[1].ext[:lin_states] - # ub = JuMP.upper_bound(state_comp.out) - # - # string_name = string(name) - # if occursin("gen", string_name) - # binaryPrecision[name] = binaryPrecisionFactor * ub - # else - # binaryPrecision[name] = 1 - # end - # end + binary_precision = Dict{Symbol, Float64}() state_approximation_regime = DynamicSDDiP.BinaryApproximation( binary_precision = binary_precision, @@ -105,6 +94,20 @@ function model_starter( ############################################################################ model = model_definition() + ############################################################################ + # DEFINE BINARY APPROXIMATION IF INTENDED + ############################################################################ + # for (name, state_comp) in model.nodes[1].ext[:lin_states] + # ub = JuMP.upper_bound(state_comp.out) + # + # string_name = string(name) + # if occursin("gen", string_name) + # binaryPrecision[name] = binaryPrecisionFactor * ub + # else + # binaryPrecision[name] = 1 + # end + # end + ############################################################################ # SOLVE MODEL ############################################################################ @@ -128,7 +131,7 @@ function model_definition() ######################################################################## # State variables - JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, Bin, initial_value = 0) + JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, initial_value = 0) # Constraints b = subproblem[:b] diff --git a/examples/subproblem.mof.json b/examples/subproblem.mof.json new file mode 100644 index 00000000..3ca466fd --- /dev/null +++ b/examples/subproblem.mof.json @@ -0,0 +1,216 @@ +{ + "name": "MathOptFormat Model", + "version": { + "major": 0, + "minor": 5 + }, + "variables": [ + { + "name": "bin_b_in[1]" + }, + { + "name": "bin_b_in[2]" + }, + { + "name": "bin_b_in[3]" + }, + { + "name": "b_in" + }, + { + "name": "b_out" + }, + { + "name": "x6" + }, + { + "name": "Θᴳ" + } + ], + "objective": { + "sense": "min", + "function": { + "type": "ScalarAffineFunction", + "terms": [ + { + "coefficient": 1.0, + "variable": "Θᴳ" + } + ], + "constant": 1.0 + } + }, + "constraints": [ + { + "name": "c1", + "function": { + "type": "ScalarAffineFunction", + "terms": [ + { + "coefficient": -1.0, + "variable": "b_in" + }, + { + "coefficient": 1.0, + "variable": "b_out" + } + ], + "constant": 0.0 + }, + "set": { + "type": "EqualTo", + "value": 1.2 + } + }, + { + "name": "c2", + "function": { + "type": "ScalarAffineFunction", + "terms": [ + { + "coefficient": -0.2857142857142857, + "variable": "bin_b_in[1]" + }, + { + "coefficient": -0.5714285714285714, + "variable": "bin_b_in[2]" + }, + { + "coefficient": -1.1428571428571428, + "variable": "bin_b_in[3]" + }, + { + "coefficient": 1.0, + "variable": "b_in" + } + ], + "constant": 0.0 + }, + "set": { + "type": "EqualTo", + "value": 0.0 + } + }, + { + "name": "c1_1", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[1]" + }, + "set": { + "type": "EqualTo", + "value": 0.0 + } + }, + { + "name": "c2_1", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[2]" + }, + "set": { + "type": "EqualTo", + "value": 0.0 + } + }, + { + "name": "c3", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[3]" + }, + "set": { + "type": "EqualTo", + "value": 1.0 + } + }, + { + "name": "c4", + "function": { + "type": "SingleVariable", + "variable": "b_in" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c5", + "function": { + "type": "SingleVariable", + "variable": "b_out" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c6", + "function": { + "type": "SingleVariable", + "variable": "x6" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c7", + "function": { + "type": "SingleVariable", + "variable": "Θᴳ" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c4_1", + "function": { + "type": "SingleVariable", + "variable": "b_in" + }, + "set": { + "type": "LessThan", + "upper": 2.0 + } + }, + { + "name": "c5_1", + "function": { + "type": "SingleVariable", + "variable": "b_out" + }, + "set": { + "type": "LessThan", + "upper": 2.0 + } + }, + { + "name": "c6_1", + "function": { + "type": "SingleVariable", + "variable": "x6" + }, + "set": { + "type": "LessThan", + "upper": 0.0 + } + }, + { + "name": "c7_1", + "function": { + "type": "SingleVariable", + "variable": "Θᴳ" + }, + "set": { + "type": "LessThan", + "upper": 0.0 + } + } + ] +} diff --git a/src/JuMP.jl b/src/JuMP.jl index b46cacac..9d4c1e1f 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -10,69 +10,68 @@ # If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ################################################################################ -function JuMP.build_variable( - _error::Function, - info::JuMP.VariableInfo, - ::Type{SDDP.State}; - initial_value = NaN, - kwargs..., -) +# function JuMP.build_variable( +# _error::Function, +# info::JuMP.VariableInfo, +# ::Type{SDDP.State}; +# initial_value = NaN, +# kwargs..., +# ) +# +# if isnan(initial_value) +# _error( +# "When creating a state variable, you must set the " * +# "`initial_value` keyword to the value of the state variable at" * +# " the root node.", +# ) +# end +# return SDDP.StateInfo( +# JuMP.VariableInfo( +# false, +# NaN, # lower bound +# false, +# NaN, # upper bound +# false, +# NaN, # fixed value +# false, +# NaN, # start value +# false, +# false, # binary and integer +# ), +# info, +# initial_value, +# kwargs, +# ) +# end - if isnan(initial_value) - _error( - "When creating a state variable, you must set the " * - "`initial_value` keyword to the value of the state variable at" * - " the root node.", - ) - end - return SDDP.StateInfo( - JuMP.VariableInfo( - false, - NaN, # lower bound - false, - NaN, # upper bound - false, - NaN, # fixed value - false, - NaN, # start value - false, - false, # binary and integer - ), - info, - initial_value, - kwargs, - ) -end +# function JuMP.add_variable( +# subproblem::JuMP.Model, +# state_info::SDDP.StateInfo, +# name::String, +# ) +# state = SDDP.State( +# JuMP.add_variable( +# subproblem, +# JuMP.ScalarVariable(state_info.in), +# name * "_in", +# ), +# JuMP.add_variable( +# subproblem, +# JuMP.ScalarVariable(state_info.out), +# name * "_out", +# ), +# ) +# node = SDDP.get_node(subproblem) +# sym_name = Symbol(name) +# @assert !haskey(node.states, sym_name) # JuMP prevents duplicate names. +# node.states[sym_name] = state +# graph = SDDP.get_policy_graph(subproblem) +# graph.initial_root_state[sym_name] = state_info.initial_value +# return state +# end -function JuMP.add_variable( - subproblem::JuMP.Model, - state_info::SDDP.StateInfo, - name::String, -) - state = SDDP.State( - JuMP.add_variable( - subproblem, - JuMP.ScalarVariable(state_info.in), - name * "_in", - ), - JuMP.add_variable( - subproblem, - JuMP.ScalarVariable(state_info.out), - name * "_out", - ), - ) - node = SDDP.get_node(subproblem) - sym_name = Symbol(name) - @assert !haskey(node.states, sym_name) # JuMP prevents duplicate names. - node.states[sym_name] = state - graph = SDDP.get_policy_graph(subproblem) - graph.initial_root_state[sym_name] = state_info.initial_value - return state -end +#JuMP.variable_type(model::JuMP.Model, ::Type{SDDP.State}) = SDDP.State - -JuMP.variable_type(model::JuMP.Model, ::Type{State}) = SDDP.State - -function JuMP.value(state::State{JuMP.VariableRef}) - return State(JuMP.value(state.in), JuMP.value(state.out)) -end +# function JuMP.value(state::State{JuMP.VariableRef}) +# return State(JuMP.value(state.in), JuMP.value(state.out)) +# end diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index b3a3f8b5..7528ad22 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -64,7 +64,7 @@ function solve( # Prepare binary_precision #--------------------------------------------------------------------------- regime = algo_params.state_approximation_regime - if regime == DynamicSDDiP.BinaryApproximation && isempty(regime.binary_precision) + if isa(regime,DynamicSDDiP.BinaryApproximation) && isempty(regime.binary_precision) # If no binary_precision dict has been defined explicitly, it is # initialized as empty. Then, for each state take a default precision. for (name, state_comp) in model.nodes[1].states @@ -81,7 +81,7 @@ function solve( # Prepare sigma #--------------------------------------------------------------------------- regime = algo_params.regularization_regime - if regime == DynamicSDDiP.Regularization && isempty(regime.sigma) + if isa(regime,DynamicSDDiP.Regularization) && isempty(regime.sigma) for (node_index, _) in model.nodes if node_index == 1 # first stage requires no regularization @@ -468,7 +468,7 @@ function iteration( binary_refinement = :none TimerOutputs.@timeit DynamicSDDiP_TIMER "bin_refinement" begin - if !isnothing(previousSolution) && bound_check + if !isnothing(previous_solution) && bound_check refinement_check = DynamicSDDiP.binary_refinement_check( model, previous_solution, @@ -499,8 +499,8 @@ function iteration( applied_solvers, forward_trajectory.scenario_path, forward_trajectory.sampled_states, - forward_trajectory.objective_states, - forward_trajectory.belief_states, + # forward_trajectory.objective_states, + # forward_trajectory.belief_states, ) end diff --git a/src/backwardPass.jl b/src/backwardPass.jl index cec3d998..d19dc6c0 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -27,8 +27,9 @@ function backward_pass( applied_solvers::DynamicSDDiP.AppliedSolvers, scenario_path::Vector{Tuple{T,NoiseType}}, sampled_states::Vector{Dict{Symbol,Float64}}, - objective_states::Vector{NTuple{N,Float64}}, - belief_states::Vector{Tuple{Int,Dict{T,Float64}}}) where {T,NoiseType,N} + # objective_states::Vector{NTuple{N,Float64}}, + # belief_states::Vector{Tuple{Int,Dict{T,Float64}}}) where {T,NoiseType,N} + ) where {T,NoiseType} ############################################################################ # INITIALIZATION @@ -70,8 +71,8 @@ function backward_pass( node_index, items, 1.0, - belief_state, - objective_state, + # belief_state, + # objective_state, outgoing_state, algo_params.backward_sampling_scheme, scenario_path[1:index], @@ -136,8 +137,8 @@ function solve_all_children( node_index::Int64, items::BackwardPassItems, belief::Float64, - belief_state, - objective_state, + # belief_state, + # objective_state, outgoing_state::Dict{Symbol,Float64}, backward_sampling_scheme::SDDP.AbstractBackwardSamplingScheme, scenario_path, @@ -250,17 +251,12 @@ function solve_subproblem_backward( changeStateSpace!(node, subproblem, state, algo_params.state_approximation_regime) end - ############################################################################ - # RESET SOLVER (as it may have been changed in between for some reason) - ############################################################################ - DynamicSDDiP.set_solver!(subproblem, algo_params, applied_solvers, :backward_pass) - ############################################################################ # SOLVE DUAL PROBLEM TO OBTAIN CUT INFORMATION ############################################################################ # Solve dual and return a dict with the multipliers of the copy constraints. TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_dual" begin - dual_results = get_dual_solution(node, node_index, solver_obj, algo_params, applied_solvers, algo_params.duality_regime) + dual_results = get_dual_solution(node, node_index, algo_params, applied_solvers, algo_params.duality_regime) end ############################################################################ diff --git a/src/binarization.jl b/src/binarization.jl index eced5283..24cfa1e2 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -54,7 +54,7 @@ function changeStateSpace!( beta = binary_precision[state_name] # Get variable info for this state to restore bounds and integer type - variable_info = node.ext[:state_info_storage][name].in + variable_info = node.ext[:state_info_storage][state_name].in # Set up state for backward pass using binary approximation setup_state_binarization!(subproblem, state_comp, state_name, beta, bw_data, variable_info) diff --git a/src/duals.jl b/src/duals.jl index 81cedd30..9bb6f6fc 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -22,7 +22,6 @@ Solving the dual problem to obtain cut information - using LP relaxation function get_dual_solution( node::SDDP.Node, node_index::Int64, - solver_obj::Float64, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, duality_regime::DynamicSDDiP.LinearDuality, @@ -119,7 +118,6 @@ and strengthening by Lagrangian relaxation function get_dual_solution( node::SDDP.Node, node_index::Int64, - solver_obj::Float64, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, duality_regime::DynamicSDDiP.StrengthenedDuality, @@ -181,7 +179,6 @@ Solving the dual problem to obtain cut information - using Lagrangian dual function get_dual_solution( node::SDDP.Node, node_index::Int64, - solver_obj::Float64, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, duality_regime::DynamicSDDiP.LagrangianDuality, @@ -217,6 +214,9 @@ function get_dual_solution( node.ext[:regularization_data] = Dict{Symbol,Any}() regularize_binary!(node, node_index, subproblem, algo_params.regularization_regime) + # RESET SOLVER (as it may have been changed in between for some reason) + DynamicSDDiP.set_solver!(subproblem, algo_params, applied_solvers, :backward_pass) + # SOLVE PRIMAL PROBLEM (can be regularized or not) TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_primal" begin JuMP.optimize!(subproblem) @@ -234,8 +234,7 @@ function get_dual_solution( ############################################################################ # GET BOUNDS FOR LAGRANGIAN DUAL ############################################################################ - bound_results = get_dual_bounds(node, algo_params, primal_obj, duality_regime.dual_bound_regime) - + bound_results = get_dual_bounds(node, node_index, algo_params, primal_obj, duality_regime.dual_bound_regime) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] try @@ -250,7 +249,6 @@ function get_dual_solution( primal_obj, dual_vars, bound_results, - integrality_handler, algo_params, applied_solvers, duality_regime.dual_solution_regime diff --git a/src/forwardPass.jl b/src/forwardPass.jl index ae30686d..1eb77c79 100644 --- a/src/forwardPass.jl +++ b/src/forwardPass.jl @@ -77,8 +77,8 @@ function forward_pass(model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, return ( scenario_path = scenario_path, sampled_states = sampled_states, - objective_states = objective_states, - belief_states = belief_states, + # objective_states = objective_states, + # belief_states = belief_states, cumulative_value = cumulative_value, ) end diff --git a/src/lagrange.jl b/src/lagrange.jl index ad13da4e..46242af7 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -85,11 +85,11 @@ function solve_lagrangian_dual( node_index::Int64, primal_obj::Float64, π_k::Vector{Float64}, - bound_results::Tuple{Float64,Float64}, + bound_results::NamedTuple{(:obj_bound, :dual_bound),Tuple{Float64,Float64}}, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.Kelley - ) + ) where ############################################################################ # INITIALIZATION @@ -437,7 +437,7 @@ function solve_lagrangian_dual( node_index::Int64, primal_obj::Float64, π_k::Vector{Float64}, - bound_results::Tuple{Float64,Float64}, + bound_results::NamedTuple{(:obj_bound, :dual_bound),Tuple{Float64,Float64}}, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.LevelBundle diff --git a/src/objective.jl b/src/objective.jl index 780b5463..0d9d53a6 100644 --- a/src/objective.jl +++ b/src/objective.jl @@ -17,12 +17,12 @@ # cost/value-to-go term. function set_objective(subproblem::JuMP.Model) node = SDDP.get_node(subproblem) - objective_state_component = SDDP.get_objective_state_component(node) - belief_state_component = SDDP.get_belief_state_component(node) - if objective_state_component != JuMP.AffExpr(0.0) || - belief_state_component != JuMP.AffExpr(0.0) - node.stage_objective_set = false - end + # objective_state_component = SDDP.get_objective_state_component(node) + # belief_state_component = SDDP.get_belief_state_component(node) + # if objective_state_component != JuMP.AffExpr(0.0) || + # belief_state_component != JuMP.AffExpr(0.0) + # node.stage_objective_set = false + # end if !node.stage_objective_set JuMP.set_objective( subproblem, @@ -30,8 +30,8 @@ function set_objective(subproblem::JuMP.Model) @expression( subproblem, node.stage_objective + - objective_state_component + - belief_state_component + + # objective_state_component + + # belief_state_component + bellman_term(node.bellman_function) ) ) diff --git a/src/regularizations.jl b/src/regularizations.jl index 74a5fe71..b061cb96 100644 --- a/src/regularizations.jl +++ b/src/regularizations.jl @@ -17,6 +17,7 @@ function regularize_subproblem!(node::SDDP.Node, node_index::Int64, # It is then subtracted from the fixed value to obtain the so called slack. reg_data = node.ext[:regularization_data] + reg_data[:fixed_state_value] = Dict{Symbol,Float64}() reg_data[:slacks] = Any[] reg_data[:reg_variables] = JuMP.VariableRef[] reg_data[:reg_constraints] = JuMP.ConstraintRef[] @@ -123,7 +124,7 @@ end """ Regularizing the backward pass problem in binary space if regularization is used. """ -function regularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64, regularization_regime::DynamicSDDiP.Regularization) +function regularize_binary!(node::SDDP.Node, node_index::Int64, subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.Regularization) bw_data = node.ext[:backward_data] binary_states = bw_data[:bin_states] @@ -150,7 +151,7 @@ function regularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Floa end end # Here, not sigma, but a different regularization parameter is used - sigma_bin = sigma * Umax + sigma_bin = regularization_regime.sigma[node_index] * Umax ############################################################################ # UNFIX THE STATE VARIABLES @@ -205,7 +206,7 @@ end Trivial regularization of the backward pass problem in binary space if no regularization is used. """ -function regularize_binary!(node::SDDP.Node, subproblem::JuMP.Model, sigma::Float64, regularization_regime::DynamicSDDiP.NoRegularization) +function regularize_binary!(node::SDDP.Node, node_index::Int64, subproblem::JuMP.Model, regularization_regime::DynamicSDDiP.NoRegularization) return end diff --git a/src/sigmaTest.jl b/src/sigmaTest.jl index 52dcb796..68eb5935 100644 --- a/src/sigmaTest.jl +++ b/src/sigmaTest.jl @@ -110,8 +110,8 @@ function forward_sigma_test( return ( scenario_path = scenario_path, sampled_states = sampled_states, - objective_states = objective_states, - belief_states = belief_states, + # objective_states = objective_states, + # belief_states = belief_states, cumulative_value = cumulative_value, sigma_increased = sigma_increased, ) diff --git a/src/solverHandling.jl b/src/solverHandling.jl index 93ab2e8a..72f2f32f 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -75,5 +75,7 @@ function set_solver!( end + JuMP.unset_silent(subproblem) + return end diff --git a/src/state.jl b/src/state.jl index cbbc6f6c..e604f29f 100644 --- a/src/state.jl +++ b/src/state.jl @@ -131,8 +131,7 @@ Note that for the Lagrangian dual (or the binarization) this is not used so far, since we do not use binary, but continuous [0,1] variables there, and thus the variables are not integer or binary anyway. -I'm not sure what the third method is for. - +The third one is used for the binary case. """ function prepare_state_fixing!(node::SDDP.Node, state_name::Symbol) if JuMP.is_binary(node.states[state_name].in) @@ -151,14 +150,14 @@ function prepare_state_fixing!(node::SDDP.Node, state::SDDP.State) end end -# function prepare_state_fixing_binary!(node::SDDP.Node, state::JuMP.VariableRef) -# if JuMP.is_binary(state) -# JuMP.unset_binary(state) -# elseif JuMP.is_integer(state) -# JuMP.unset_integer(state) -# end -# return -# end +function prepare_state_fixing_binary!(node::SDDP.Node, state::JuMP.VariableRef) + if JuMP.is_binary(state) + JuMP.unset_binary(state) + elseif JuMP.is_integer(state) + JuMP.unset_integer(state) + end + return +end ################################################################################ @@ -212,7 +211,7 @@ function follow_state_unfixing!(state::SDDP.State, variable_info::DynamicSDDiP.V JuMP.set_lower_bound(state.in, variable_info.lower_bound) end if variable_info.has_ub - JuMP.set_upper_bound(state.in, variable_info.in.upper_bound) + JuMP.set_upper_bound(state.in, variable_info.upper_bound) end if variable_info.binary JuMP.set_binary(state.in) diff --git a/stuff.jl b/stuff.jl index 55519db3..fc531244 100644 --- a/stuff.jl +++ b/stuff.jl @@ -40,13 +40,26 @@ typeof(return_results) using JuMP using GLPK -approx_model = JuMP.Model(GLPK.Optimizer) +using GAMS +approx_model = JuMP.Model(GAMS.Optimizer) +set_optimizer(approx_model, optimizer_with_attributes( + GAMS.Optimizer, + "Solver"=>"Gurobi", + "optcr"=>0.0, + ) +) ν = JuMP.@variable(approx_model, [1:10], lower_bound=0) typeof(ν) @variable(approx_model, t <= 100) @objective(approx_model, Max, t) +JuMP.unset_silent(approx_model) +JuMP.optimize!(approx_model) +JuMP.set_silent(approx_model) +JuMP.optimize!(approx_model) + + # Create the dual variables # Note that the real dual multipliers are split up into two non-negative # variables here, which is required for the Magnanti Wong part later From 05295f76975d218b621fc193ba1eae730b0d5111 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Thu, 21 Oct 2021 11:06:28 -0400 Subject: [PATCH 28/30] A lot of corrections when testing a simple discontinuous example --- examples/newExample_1.jl | 33 ++++-- examples/subproblem.mof.json | 222 +++++++++++++++++++++++++++++------ src/algorithmMain.jl | 33 ++++-- src/backwardPass.jl | 5 +- src/bellman.jl | 49 ++++---- src/binarization.jl | 4 +- src/binaryRefinement.jl | 2 +- src/cutSelection.jl | 23 +++- src/duals.jl | 10 +- src/lagrange.jl | 44 ++++--- src/logging.jl | 116 +++++++++--------- src/regularizations.jl | 9 +- src/sigmaTest.jl | 12 +- src/solverHandling.jl | 4 +- src/stopping.jl | 19 ++- src/typedefs.jl | 2 +- 16 files changed, 406 insertions(+), 181 deletions(-) diff --git a/examples/newExample_1.jl b/examples/newExample_1.jl index 39496ebc..0be31f0b 100644 --- a/examples/newExample_1.jl +++ b/examples/newExample_1.jl @@ -24,7 +24,7 @@ function model_config() dual_solution_regime = DynamicSDDiP.Kelley() dual_bound_regime = DynamicSDDiP.BothBounds() dual_status_regime = DynamicSDDiP.Rigorous() - dual_choice_regime = DynamicSDDiP.StandardChoice() + dual_choice_regime = DynamicSDDiP.MagnantiWongChoice() duality_regime = DynamicSDDiP.LagrangianDuality( atol = 1e-8, rtol = 1e-8, @@ -37,7 +37,7 @@ function model_config() ) # State approximation and cut projection configuration - cut_projection_regime = DynamicSDDiP.BigM() + cut_projection_regime = DynamicSDDiP.SOS1() binary_precision = Dict{Symbol, Float64}() state_approximation_regime = DynamicSDDiP.BinaryApproximation( @@ -129,16 +129,33 @@ function model_definition() ######################################################################## # DEFINE STAGE-t MODEL ######################################################################## - # State variables JuMP.@variable(subproblem, 0.0 <= b <= 2.0, SDDP.State, initial_value = 0) - # Constraints - b = subproblem[:b] - JuMP.@constraint(subproblem, b.out == 1.2 + b.in) + if t == 1 + + # Constraints + b = subproblem[:b] + JuMP.@constraint(subproblem, b.out == 1.2 + b.in) + + # Stage objective + SDDP.@stageobjective(subproblem, 1) + + else + # Local variables + JuMP.@variable(subproblem, 0.0 <= x[i=1:4]) + JuMP.set_integer(x[1]) + JuMP.set_integer(x[2]) + + # Constraints + b = subproblem[:b] + JuMP.@constraint(subproblem, b.out == 0) + JuMP.@constraint(subproblem, con, 1.25*x[1] - x[2] + 0.5*x[3] + 1/3*x[4] == b.in) + + # Stage objective + SDDP.@stageobjective(subproblem, x[1] - 0.75*x[2] + 0.75*x[3] + 2.5*x[4]) - # Stage objective - SDDP.@stageobjective(subproblem, 1) + end end diff --git a/examples/subproblem.mof.json b/examples/subproblem.mof.json index 3ca466fd..6e2f47d6 100644 --- a/examples/subproblem.mof.json +++ b/examples/subproblem.mof.json @@ -6,25 +6,37 @@ }, "variables": [ { - "name": "bin_b_in[1]" + "name": "b_in" }, { - "name": "bin_b_in[2]" + "name": "b_out" }, { - "name": "bin_b_in[3]" + "name": "x[1]" }, { - "name": "b_in" + "name": "x[2]" }, { - "name": "b_out" + "name": "x[3]" + }, + { + "name": "x[4]" }, { - "name": "x6" + "name": "x7" }, { "name": "Θᴳ" + }, + { + "name": "bin_b_in[1]" + }, + { + "name": "bin_b_in[2]" + }, + { + "name": "bin_b_in[3]" } ], "objective": { @@ -32,17 +44,50 @@ "function": { "type": "ScalarAffineFunction", "terms": [ + { + "coefficient": 1.0, + "variable": "x[1]" + }, + { + "coefficient": -0.75, + "variable": "x[2]" + }, + { + "coefficient": 0.75, + "variable": "x[3]" + }, + { + "coefficient": 2.5, + "variable": "x[4]" + }, { "coefficient": 1.0, "variable": "Θᴳ" } ], - "constant": 1.0 + "constant": 0.0 } }, "constraints": [ { "name": "c1", + "function": { + "type": "ScalarAffineFunction", + "terms": [ + { + "coefficient": 1.0, + "variable": "b_out" + } + ], + "constant": 0.0 + }, + "set": { + "type": "EqualTo", + "value": 0.0 + } + }, + { + "name": "con", "function": { "type": "ScalarAffineFunction", "terms": [ @@ -51,22 +96,38 @@ "variable": "b_in" }, { - "coefficient": 1.0, - "variable": "b_out" + "coefficient": 1.25, + "variable": "x[1]" + }, + { + "coefficient": -1.0, + "variable": "x[2]" + }, + { + "coefficient": 0.5, + "variable": "x[3]" + }, + { + "coefficient": 0.3333333333333333, + "variable": "x[4]" } ], "constant": 0.0 }, "set": { "type": "EqualTo", - "value": 1.2 + "value": 0.0 } }, { - "name": "c2", + "name": "c3", "function": { "type": "ScalarAffineFunction", "terms": [ + { + "coefficient": 1.0, + "variable": "b_in" + }, { "coefficient": -0.2857142857142857, "variable": "bin_b_in[1]" @@ -78,10 +139,6 @@ { "coefficient": -1.1428571428571428, "variable": "bin_b_in[3]" - }, - { - "coefficient": 1.0, - "variable": "b_in" } ], "constant": 0.0 @@ -95,40 +152,40 @@ "name": "c1_1", "function": { "type": "SingleVariable", - "variable": "bin_b_in[1]" + "variable": "b_in" }, "set": { - "type": "EqualTo", - "value": 0.0 + "type": "GreaterThan", + "lower": 0.0 } }, { - "name": "c2_1", + "name": "c2", "function": { "type": "SingleVariable", - "variable": "bin_b_in[2]" + "variable": "b_out" }, "set": { - "type": "EqualTo", - "value": 0.0 + "type": "GreaterThan", + "lower": 0.0 } }, { - "name": "c3", + "name": "c3_1", "function": { "type": "SingleVariable", - "variable": "bin_b_in[3]" + "variable": "x[1]" }, "set": { - "type": "EqualTo", - "value": 1.0 + "type": "GreaterThan", + "lower": 0.0 } }, { "name": "c4", "function": { "type": "SingleVariable", - "variable": "b_in" + "variable": "x[2]" }, "set": { "type": "GreaterThan", @@ -139,7 +196,7 @@ "name": "c5", "function": { "type": "SingleVariable", - "variable": "b_out" + "variable": "x[3]" }, "set": { "type": "GreaterThan", @@ -150,7 +207,7 @@ "name": "c6", "function": { "type": "SingleVariable", - "variable": "x6" + "variable": "x[4]" }, "set": { "type": "GreaterThan", @@ -159,6 +216,17 @@ }, { "name": "c7", + "function": { + "type": "SingleVariable", + "variable": "x7" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c8", "function": { "type": "SingleVariable", "variable": "Θᴳ" @@ -169,7 +237,40 @@ } }, { - "name": "c4_1", + "name": "c9", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[1]" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c10", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[2]" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c11", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[3]" + }, + "set": { + "type": "GreaterThan", + "lower": 0.0 + } + }, + { + "name": "c1_2", "function": { "type": "SingleVariable", "variable": "b_in" @@ -180,7 +281,7 @@ } }, { - "name": "c5_1", + "name": "c2_1", "function": { "type": "SingleVariable", "variable": "b_out" @@ -191,10 +292,10 @@ } }, { - "name": "c6_1", + "name": "c7_1", "function": { "type": "SingleVariable", - "variable": "x6" + "variable": "x7" }, "set": { "type": "LessThan", @@ -202,7 +303,7 @@ } }, { - "name": "c7_1", + "name": "c8_1", "function": { "type": "SingleVariable", "variable": "Θᴳ" @@ -211,6 +312,59 @@ "type": "LessThan", "upper": 0.0 } + }, + { + "name": "c9_1", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[1]" + }, + "set": { + "type": "LessThan", + "upper": 1.0 + } + }, + { + "name": "c10_1", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[2]" + }, + "set": { + "type": "LessThan", + "upper": 1.0 + } + }, + { + "name": "c11_1", + "function": { + "type": "SingleVariable", + "variable": "bin_b_in[3]" + }, + "set": { + "type": "LessThan", + "upper": 1.0 + } + }, + { + "name": "c3_2", + "function": { + "type": "SingleVariable", + "variable": "x[1]" + }, + "set": { + "type": "Integer" + } + }, + { + "name": "c4_1", + "function": { + "type": "SingleVariable", + "variable": "x[2]" + }, + "set": { + "type": "Integer" + } } ] } diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index 7528ad22..837ad719 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -97,6 +97,7 @@ function solve( options = DynamicSDDiP.Options( model, model.initial_root_state, + algo_params.risk_measure, time(), log, log_file_handle @@ -158,14 +159,16 @@ function solve( finally end + @infiltrate + ############################################################################ # lOG MODEL RESULTS ############################################################################ results = DynamicSDDiP.Results(status, log) model.ext[:results] = results - if print_level > 0 + if algo_params.print_level > 0 print_helper(print_footer, log_file_handle, results) - if print_level > 1 + if algo_params.print_level > 1 print_helper(TimerOutputs.print_timer, log_file_handle, DynamicSDDiP_TIMER) print_helper(println, log_file_handle) end @@ -208,7 +211,6 @@ function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGrap for (i, (name, state)) in enumerate(node.states) variable_info_in = get_variable_info(state.in) variable_info_out = get_variable_info(state.out) - node.ext[:state_info_storage][name] = DynamicSDDiP.StateInfoStorage(variable_info_in, variable_info_out) end @@ -216,6 +218,14 @@ function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGrap @infiltrate algo_params.infiltrate_state == :all + ############################################################################ + # LOG ITERATION HEADER + ############################################################################ + if algo_params.print_level > 0 + print_helper(io -> println(io, "Solver: ", parallel_scheme, "\n"), options.log_file_handle) + print_helper(print_iteration_header, options.log_file_handle) + end + ############################################################################ # CALL ACTUAL SOLUTION PROCEDURE ############################################################################ @@ -285,6 +295,10 @@ function master_loop(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, algo_params.regularization_regime ) + if result.has_converged + return result.status + end + previous_solution = convergence_results.previous_solution previous_bound = convergence_results.previous_bound sigma_increased = convergence_results.sigma_increased @@ -300,7 +314,8 @@ end Convergence handler if regularization is used. """ -function convergence_handler(result::DynamicSDDiP.IterationResult, +function convergence_handler( + result::DynamicSDDiP.IterationResult, model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, @@ -325,12 +340,12 @@ function convergence_handler(result::DynamicSDDiP.IterationResult, previous_bound = nothing # binary refinement only when no sigma refinement has been made bound_check = false + # no convergence + result.has_converged = false else #################################################################### - # THE ALGORITHM TERMINATES + # THE ALGORITHM WILL TERMINATE #################################################################### - # return convergence status - return result.status end ############################################################################ @@ -369,7 +384,7 @@ function convergence_handler(result::DynamicSDDiP.IterationResult, sigma_increased = sigma_increased, previous_solution = previous_solution, previous_bound = previous_bound, - bound_check = bound_check + bound_check = bound_check, ) end @@ -512,6 +527,8 @@ function iteration( end bound = first_stage_results.bound + #@infiltrate + ############################################################################ # CHECK IF BEST KNOWN SOLUTION HAS BEEN IMPROVED ############################################################################ diff --git a/src/backwardPass.jl b/src/backwardPass.jl index d19dc6c0..8c1a2f64 100644 --- a/src/backwardPass.jl +++ b/src/backwardPass.jl @@ -83,8 +83,7 @@ function backward_pass( ######################################################################## # RECONSTRUCT ANCHOR POINTS IN BACKWARD PASS ######################################################################## - variable_info = node.ext[:state_info_storage][name].out - anchor_states = determine_anchor_states(node, outgoing_state, algo_params.state_approximation_regime, variable_info) + anchor_states = determine_anchor_states(node, outgoing_state, algo_params.state_approximation_regime) @infiltrate algo_params.infiltrate_state in [:all] ######################################################################## @@ -369,7 +368,6 @@ function solve_first_stage_problem( state = get_outgoing_state(node) stage_objective = JuMP.value(node.stage_objective) objective = JuMP.objective_value(subproblem) - dual_values = get_dual_variables(node, node.integrality_handler) ############################################################################ # DETERMINE THE PROBLEM SIZE @@ -384,7 +382,6 @@ function solve_first_stage_problem( return ( state = state, - duals = dual_values, objective = objective, stage_objective = stage_objective, problem_size = problem_size diff --git a/src/bellman.jl b/src/bellman.jl index c8bd00fd..b78d641b 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -323,7 +323,7 @@ function _add_cut( for (key, λ) in λᵏ θᵏ -= πᵏ[key] * λᵏ[key].value end - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # CONSTRUCT NONLINEAR CUT STRUCT @@ -421,7 +421,7 @@ function add_cut_constraints_to_models( variable_info = node.ext[:state_info_storage][state_name].out - if !isfinite(variable_info.upper_bound) || !variable_info.has_ub || !variable_info.binary + if (!isfinite(variable_info.upper_bound) || !variable_info.has_ub) && !variable_info.binary error("When using DynamicSDDiP, state variables require an upper bound.") end @@ -430,12 +430,12 @@ function add_cut_constraints_to_models( #################################################################### if variable_info.binary beta = 1 - K = SDDP._bitsrequired(variable_info.upper_bound) + K = 1 elseif variable_info.integer beta = 1 K = SDDP._bitsrequired(variable_info.upper_bound) else - beta = cut.binary_precision[name] + beta = cut.binary_precision[state_name] K = SDDP._bitsrequired(round(Int, variable_info.upper_bound / beta)) end @@ -448,7 +448,7 @@ function add_cut_constraints_to_models( model, node, ######################################################## - V.states[name], # state_comp + V.states[state_name], # state_comp state_name, state_index, ######################################################## @@ -475,7 +475,7 @@ function add_cut_constraints_to_models( end - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # MAKE SOME VALIDITY CHECKS @@ -503,7 +503,7 @@ function add_cut_constraints_to_models( ############################################################################ # ADD SOS1 STRONG DUALITY CONSTRAINT ############################################################################ - add_strong_duality_cut(model, node, cut, V, all_lambda, all_mu, all_eta, + add_strong_duality_cut!(model, node, cut, V, all_lambda, all_mu, all_eta, all_coefficients, number_of_states, number_of_duals, algo_params.state_approximation_regime.cut_projection_regime) @@ -665,7 +665,7 @@ function add_complementarity_constraints!( cut_projection_regime::DynamicSDDiP.KKT, ) - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # ADD COMPLEMENTARITY CONSTRAINTS @@ -717,7 +717,7 @@ function add_complementarity_constraints!( cut_projection_regime::DynamicSDDiP.BigM, ) - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # ADD ADDITIONAL BINARY VARIABLES @@ -780,10 +780,19 @@ function get_bigM(node::SDDP.Node, sigma::Float64, beta::Float64, related_coeffi ############################################################################ U_max = 0 for (i, (name, state)) in enumerate(node.states) + variable_info = node.ext[:state_info_storage][name].out + + upper_bound = -Inf + if variable_info.binary + upper_bound = 1 + elseif variable_info.has_ub + upper_bound = variable_info.upper_bound + end - if JuMP.upper_bound(state.in) > U_max - U_max = JuMP.upper_bound(state.in) + if upper_bound > U_max + U_max = upper_bound end + end ############################################################################ @@ -791,7 +800,7 @@ function get_bigM(node::SDDP.Node, sigma::Float64, beta::Float64, related_coeffi ############################################################################ bigM = 0 for k in 1:K - candidate = Umax * (sigma + abs(relatedCoefficients[k]) / (2^(k-1) * beta)) + candidate = U_max * (sigma + abs(related_coefficients[k]) / (2^(k-1) * beta)) if bigM < candidate bigM = candidate end @@ -830,7 +839,7 @@ function add_complementarity_constraints!( cut_projection_regime::DynamicSDDiP.SOS1, ) - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # AUXILIARY VARIABLE @@ -958,7 +967,7 @@ function set_up_dict_for_duals( return Dict(key => 0.0 for key in keys(trial_points)) end -function validity_checks( +function validity_checks!( cut::DynamicSDDiP.NonlinearCut, V::DynamicSDDiP.CutApproximation, K_tilde::Int64, @@ -976,13 +985,13 @@ function validity_checks( == size(all_lambda, 1) ) @assert (number_of_states == size(all_eta, 1) - == size(V.states, 1) + == length(V.states) ) return end -function validity_checks( +function validity_checks!( cut::DynamicSDDiP.NonlinearCut, V::DynamicSDDiP.CutApproximation, K_tilde::Int64, @@ -1023,7 +1032,7 @@ function get_cut_expression( V.theta - sum(all_coefficients[j] * all_lambda[j] for j in 1:number_of_duals) ) - return + return expr end function get_cut_expression( @@ -1045,7 +1054,7 @@ function get_cut_expression( - sum(x * all_eta[i] for (i, x) in V.states) ) - return + return expr end function add_strong_duality_cut!( @@ -1126,7 +1135,7 @@ function _add_cut( for (key, x) in xᵏ θᵏ -= πᵏ[key] * x end - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # CONSTRUCT NONLINEAR CUT STRUCT @@ -1178,7 +1187,7 @@ function add_cut_constraints_to_models( model = JuMP.owner_model(V.theta) @assert model == node.subproblem - @infiltrate infiltrate_state in [:bellman] + @infiltrate infiltrate_state in [:bellman, :all] ############################################################################ # ADD THE LINEAR CUT CONSTRAINT diff --git a/src/binarization.jl b/src/binarization.jl index 24cfa1e2..733c2f53 100644 --- a/src/binarization.jl +++ b/src/binarization.jl @@ -324,6 +324,7 @@ function setup_state_binarization!( #################################################################### # Get fixed values from fixed value of original state fixed_binary_values = SDDP.binexpand(bw_data[:fixed_state_value][state_name], variable_info.upper_bound, beta) + # Fix binary variables for i in 1:num_vars #JuMP.unset_binary(binary_vars[i].in) @@ -357,7 +358,8 @@ function determine_anchor_states( for (name, value) in outgoing_state state_comp = node.states[name] beta = state_approximation_regime.binary_precision[name] - (approx_state_value, ) = determine_anchor_state(state_comp, value, beta) + variable_info = node.ext[:state_info_storage][name].out + (approx_state_value, ) = determine_anchor_state(state_comp, value, beta, variable_info) anchor_states[name] = approx_state_value end diff --git a/src/binaryRefinement.jl b/src/binaryRefinement.jl index 8e44f441..ae2483db 100644 --- a/src/binaryRefinement.jl +++ b/src/binaryRefinement.jl @@ -73,7 +73,7 @@ function binary_refinement( # Precision is only used (and increased) for continuous variables for (name, state_comp) in model.nodes[2].states - variable_info = node.ext[:state_info_storage][name].in + variable_info = model.nodes[2].ext[:state_info_storage][name].in if !variable_info.binary && !variable_info.integer current_prec = binary_precision[name] diff --git a/src/cutSelection.jl b/src/cutSelection.jl index c1a5218e..373a5b47 100644 --- a/src/cutSelection.jl +++ b/src/cutSelection.jl @@ -29,6 +29,25 @@ function _cut_selection_update( cut_selection_regime::DynamicSDDiP.NoCutSelection ) + ############################################################################ + # ADD CUTS AND STATES TO THE ORACLE + ############################################################################ + oracle = V.cut_oracle + + sampled_state_anchor = DynamicSDDiP.SampledState(anchor_state, cut, _eval_height(node, cut, + anchor_state, applied_solvers, algo_params)) + sampled_state_trial = DynamicSDDiP.SampledState(trial_state, cut, _eval_height(node, cut, trial_state, applied_solvers, algo_params)) + + push!(oracle.states, sampled_state_anchor) + push!(oracle.states, sampled_state_trial) + + push!(oracle.cuts, cut) + + ############################################################################ + # DETERMINE NUMBER OF CUTS FOR LOGGING + ############################################################################ + count_cuts(node, V) + return end @@ -263,7 +282,7 @@ function _cut_selection_update( ############################################################################ # DETERMINE NUMBER OF CUTS FOR LOGGING ############################################################################ - counts_cuts(node, V) + count_cuts(node, V) end @@ -320,7 +339,7 @@ function _eval_height(node::SDDP.Node, cut::DynamicSDDiP.NonlinearCut, all_coefficients = Float64[] binary_variables_so_far = 0 - for (i, (state_name, state_comp)) in enumerate(node.states) + for (i, (state_name, value)) in enumerate(states) variable_info = node.ext[:state_info_storage][state_name].out if variable_info.binary diff --git a/src/duals.jl b/src/duals.jl index 9bb6f6fc..ba7dccce 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -275,7 +275,7 @@ function get_dual_solution( ############################################################################ # SET DUAL VARIABLES AND STATES CORRECTLY FOR RETURN ############################################################################ - store_dual_values!(node, dual_values, dual_vars, binary_state, integrality_handler, algo_params.state_approximation_regime) + store_dual_values!(node, dual_values, dual_vars, bin_state, algo_params.state_approximation_regime) return ( dual_values=dual_values, @@ -499,14 +499,16 @@ function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Flo return end -function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, +function store_dual_values!(node::SDDP.Node, dual_values::Dict{Symbol, Float64}, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, state_approximation_regime::DynamicSDDiP.BinaryApproximation) + old_rhs = node.ext[:backward_data][:old_rhs] + for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) dual_values[name] = dual_vars[i] - value = integrality_handler.old_rhs[i] + value = old_rhs[i] x_name = node.ext[:backward_data][:bin_x_names][name] k = node.ext[:backward_data][:bin_k][name] bin_state[name] = BinaryState(value, x_name, k) @@ -515,7 +517,7 @@ function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, return end -function store_dual_values!(node::SDDP.Node, dual_values::Vector{Float64}, +function store_dual_values!(node::SDDP.Node, dual_values::Dict{Symbol, Float64}, dual_vars::Vector{Float64}, bin_state::Dict{Symbol, BinaryState}, state_approximation_regime::DynamicSDDiP.NoStateApproximation) diff --git a/src/lagrange.jl b/src/lagrange.jl index 46242af7..0967b822 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -89,7 +89,7 @@ function solve_lagrangian_dual( algo_params::DynamicSDDiP.AlgoParams, applied_solvers::DynamicSDDiP.AppliedSolvers, dual_solution_regime::DynamicSDDiP.Kelley - ) where + ) ############################################################################ # INITIALIZATION @@ -126,7 +126,8 @@ function solve_lagrangian_dual( ############################################################################ # RELAXING THE COPY CONSTRAINTS ############################################################################ - relax_copy_constraints(node, x_in_value, h_expr, algo_params.state_approximation_regime) + relax_copy_constraints!(node, x_in_value, h_expr, algo_params.state_approximation_regime) + node.ext[:backward_data][:old_rhs] = x_in_value ############################################################################ # LOGGING OF LAGRANGIAN DUAL @@ -139,7 +140,7 @@ function solve_lagrangian_dual( ############################################################################ # Approximation of Lagrangian dual by cutting planes # Optimizer is re-set anyway - approx_model = JuMP.Model(GLPK.Optimizer) + approx_model = JuMP.Model(Gurobi.Optimizer) set_solver!(approx_model, algo_params, applied_solvers, :kelley) # Create the objective @@ -164,7 +165,8 @@ function solve_lagrangian_dual( lag_status = :none # set up optimal value of approx_model (former f_approx) - t_k = -Inf + t_k = 0 # why zero? + #-inf is not possible, since then the while loop would not start at all while iter <= iteration_limit && !isapprox(L_star, t_k, atol = atol, rtol = rtol) iter += 1 @@ -196,7 +198,7 @@ function solve_lagrangian_dual( JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL t_k = JuMP.objective_value(approx_model) - π_k .= JuMP.value(π) + π_k .= JuMP.value.(π) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] #print("UB: ", f_approx, ", LB: ", f_actual) @@ -235,8 +237,8 @@ function solve_lagrangian_dual( ############################################################################ # APPLY MAGNANTI AND WONG APPROACH IF INTENDED ############################################################################ - magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, - iteration_limit, atol, rtol, algo_params.dual_choice_regime) + magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_star, + iteration_limit, atol, rtol, algo_params.duality_regime.dual_choice_regime, iter) ############################################################################ # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) @@ -251,7 +253,7 @@ function solve_lagrangian_dual( ############################################################################ # LOGGING ############################################################################ - print_helper(print_lag_iteration, lag_log_file_handle, iter, t_k, L_star, L_k) + # print_helper(print_lag_iteration, lag_log_file_handle, iter, t_k, L_star, L_k) # Set dual_vars (here π_k) to the optimal solution π_k = π_star @@ -270,16 +272,16 @@ function relax_copy_constraints!( for (i, (_, state)) in enumerate(node.ext[:backward_data][:bin_states]) # Store original value of ̄x, which z was fixed to - x_in_value[i] = JuMP.fix_value(bin_state) + x_in_value[i] = JuMP.fix_value(state) # Store expression for slack - h_expr[i] = @expression(node.subproblem, bin_state - x_in_value[i]) + h_expr[i] = @expression(node.subproblem, state - x_in_value[i]) # Relax copy constraint (i.e. z does not have to take the value of ̄x anymore) - JuMP.unfix(bin_state) + JuMP.unfix(state) # Set bounds to ensure that inner problems are feasible # As we use binary approximation, 0 and 1 can be used - JuMP.set_lower_bound(bin_state, 0) - JuMP.set_upper_bound(bin_state, 1) + JuMP.set_lower_bound(state, 0) + JuMP.set_upper_bound(state, 1) end @@ -350,6 +352,9 @@ end function set_multiplier_bounds!(approx_model::JuMP.Model, number_of_states::Int, dual_bound::Float64) + π⁺ = approx_model[:π⁺] + π⁻ = approx_model[:π⁻] + for i in 1:number_of_states JuMP.set_upper_bound(π⁺[i], dual_bound) JuMP.set_upper_bound(π⁻[i], dual_bound) @@ -373,14 +378,19 @@ function magnanti_wong!( h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, h_k::Vector{Float64}, s::Int, - L_k::Float64, L_star::Float64, iteration_limit::Int, atol::Float64, rtol::Float64, dual_choice_regime::DynamicSDDiP.MagnantiWongChoice, + iter::Int, ) + π⁺ = approx_model[:π⁺] + π⁻ = approx_model[:π⁻] + t = approx_model[:t] + π = approx_model[:π] + # Reset objective @objective(approx_model, Min, sum(π⁺) + sum(π⁻)) JuMP.set_lower_bound(t, t_k) @@ -392,7 +402,7 @@ function magnanti_wong!( JuMP.optimize!(approx_model) @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL π_k .= value.(π) - L_k = _solve_Lagrangian_relaxation(node.subproblem, π_k, h_expr, h_k) + L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) if isapprox(L_star, L_k, atol = atol, rtol = rtol) # At this point we tried the smallest ‖π‖ from the cutting plane # problem, and it returned the optimal dual objective value. No @@ -416,12 +426,12 @@ function magnanti_wong!( h_expr::Vector{GenericAffExpr{Float64,VariableRef}}, h_k::Vector{Float64}, s::Int, - L_k::Float64, L_star::Float64, iteration_limit::Int, atol::Float64, rtol::Float64, dual_choice_regime::DynamicSDDiP.StandardChoice, + iter::Int, ) return @@ -670,7 +680,7 @@ function solve_lagrangian_dual( # APPLY MAGNANTI AND WONG APPROACH IF INTENDED ############################################################################ magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, - iteration_limit, atol, rtol, algo_params.dual_choice_regime) + iteration_limit, atol, rtol, algo_params.dual_choice_regime, iter) ############################################################################ # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) diff --git a/src/logging.jl b/src/logging.jl index 12d7d1b3..f8625101 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -47,6 +47,8 @@ struct Options{T} # Storage for the set of possible sampling states at each node. We only use # this if there is a cycle in the policy graph. starting_states::Dict{T,Vector{Dict{Symbol,Float64}}} + # Risk measure to use at each node. + risk_measures::Dict{T,SDDP.AbstractRiskMeasure} # The node transition matrix. Φ::Dict{Tuple{T,T},Float64} # A list of nodes that contain a subset of the children of node i. @@ -59,6 +61,7 @@ struct Options{T} function Options( model::SDDP.PolicyGraph{T}, initial_state::Dict{Symbol,Float64}, + risk_measures, start_time::Float64, log::Vector{DynamicSDDiP.Log}, log_file_handle::Any @@ -66,6 +69,7 @@ struct Options{T} return new{T}( initial_state, SDDP.to_nodal_form(model, x -> Dict{Symbol,Float64}[]), + SDDP.to_nodal_form(model, risk_measures), SDDP.build_Φ(model), SDDP.get_same_children(model), start_time, @@ -86,10 +90,9 @@ function print_helper(f, io, args...) end function print_banner(io) - println( - io, - "--------------------------------------------------------------------------------", - ) + println(io,"#########################################################################################################################################",) + println(io,"#########################################################################################################################################",) + println(io,"#########################################################################################################################################",) println(io, "DynamicSDDiP.jl (c) Christian Füllner, 2021") println(io, "re-uses code from SDDP.jl (c) Oscar Dowson, 2017-21") println(io) @@ -113,76 +116,77 @@ function print_parameters(io, algo_params::DynamicSDDiP.AlgoParams, applied_solv if isempty(algo_params.stopping_rules) println(io, "No stopping rule defined.") else - for i in algo_params.stopping_rules - if isa(algo_params.stopping_rules[i], DeterministicStopping) - println(io, Printf.@sprintf("opt_rtol: %1.4e", algo_params.opt_rtol)) - println(io, Printf.@sprintf("opt_atol: %1.4e", algo_params.opt_atol)) - elseif isa(algo_params.stopping_rules[i], SDDP.IterationLimit) - println(io, Printf.@sprintf("iteration_limit: %5d", algo_params.iteration_limit)) - elseif isa(algo_params.stopping_rules[i], ) - println(io, Printf.@sprintf("time_limit (sec): %6d", algo_params.time_limit)) + for i in 1:size(algo_params.stopping_rules,1) + rule = algo_params.stopping_rules[i] + if isa(rule, DeterministicStopping) + println(io, Printf.@sprintf("opt_rtol: %1.4e", rule.rtol)) + println(io, Printf.@sprintf("opt_atol: %1.4e", rule.atol)) + elseif isa(rule, SDDP.IterationLimit) + println(io, Printf.@sprintf("iteration_limit: %5d", rule.iteration_limit)) + elseif isa(rule, SDDP.TimeLimit) + println(io, Printf.@sprintf("time_limit (sec): %6d", rule.time_limit)) end end end - println(io, "------------------------------------------------------------------------") + println(io, "----------------------------------------------------------------------------------------------------------------------------------------") print(io, "Binary approximation used: ") println(io, algo_params.state_approximation_regime) - if algo_params.state_approximation_regime == DynamicSDDiP.BinaryApproximation + if isa(algo_params.state_approximation_regime, DynamicSDDiP.BinaryApproximation) state_approximation_regime = algo_params.state_approximation_regime print(io, "Initial binary precision: ") println(io, state_approximation_regime.binary_precision) print(io, "Cut projection method: ") - println(io, state_approximation_regime.cut_projection_method) + println(io, state_approximation_regime.cut_projection_regime) end - println(io, "------------------------------------------------------------------------") + println(io, "----------------------------------------------------------------------------------------------------------------------------------------") print(io, "Regularization used: ") println(io, algo_params.regularization_regime) - if algo_params.regularization_regime == DynamicSDDiP.Regularization - println(io, Printf.@sprintf("Initial sigma: %4.1e", algo_params.sigma)) - println(io, Printf.@sprintf("Sigma increase factor: %4.1e", algo_params.sigma:factor)) - end + #if isa(algo_params.regularization_regime, DynamicSDDiP.Regularization) + # println(io, Printf.@sprintf("Initial sigma: %4.1e", algo_params.sigma)) + # println(io, Printf.@sprintf("Sigma increase factor: %4.1e", algo_params.sigma:factor)) + #end - println(io, "------------------------------------------------------------------------") + println(io, "----------------------------------------------------------------------------------------------------------------------------------------") print(io, "Cut family used: ") println(io, algo_params.duality_regime) - if algo_params.duality_regime == DynamicSDDiP.LagrangianDuality - duality_regime = algo_params.duality_regime - print(io, "Dual initialization: ") - println(io, duality_regime.dual_initialization_regime) - print(io, "Dual bounding: ") - println(io, duality_regime.dual_bound_regime) - print(io, "Dual solution method: ") - println(io, duality_regime.dual_solution_regime) - print(io, "Dual multiplier choice: ") - println(io, duality_regime.dual_choice_regime) - print(io, "Dual status regime: ") - println(io, duality_regime.dual_status_regime) - #print(io, "Numerical focus used: ") - #println(io, duality_regime.numerical_focus) - println(io, "------------------------------------------------------------------------") - dual_solution_regime = duality_regime.dual_solution_regime - println(io, Printf.@sprintf("Lagrangian rtol: %1.4e", dual_solution_regime.rtol)) - println(io, Printf.@sprintf("Lagrangian atol: %1.4e", dual_solution_regime.atol)) - println(io, Printf.@sprintf("iteration_limit: %5d", dual_solution_regime.iteration_limit)) - if dual_solution_regime == DynamicSDDiP.LevelBundle - println(io, Printf.@sprintf("Level parameter: %2.4e", dual_solution_regime.level_factor)) - println(io, Printf.@sprintf("Bundle alpha: %2.4e", dual_solution_regime.bundle_alpha)) - println(io, Printf.@sprintf("Bundle factor: %2.4e", dual_solution_regime.bundle_factor)) - end - println(io, "------------------------------------------------------------------------") - - end + # if isa(algo_params.duality_regime, DynamicSDDiP.LagrangianDuality) + # duality_regime = algo_params.duality_regime + # print(io, "Dual initialization: ") + # println(io, duality_regime.dual_initialization_regime) + # print(io, "Dual bounding: ") + # println(io, duality_regime.dual_bound_regime) + # print(io, "Dual solution method: ") + # println(io, duality_regime.dual_solution_regime) + # print(io, "Dual multiplier choice: ") + # println(io, duality_regime.dual_choice_regime) + # print(io, "Dual status regime: ") + # println(io, duality_regime.dual_status_regime) + # #print(io, "Numerical focus used: ") + # #println(io, duality_regime.numerical_focus) + # println(io, "----------------------------------------------------------------------------------------------------------------------------------------") + # dual_solution_regime = duality_regime.dual_solution_regime + # println(io, Printf.@sprintf("Lagrangian rtol: %1.4e", dual_solution_regime.rtol)) + # println(io, Printf.@sprintf("Lagrangian atol: %1.4e", dual_solution_regime.atol)) + # println(io, Printf.@sprintf("iteration_limit: %5d", dual_solution_regime.iteration_limit)) + # if isa(dual_solution_regime, DynamicSDDiP.LevelBundle) + # println(io, Printf.@sprintf("Level parameter: %2.4e", dual_solution_regime.level_factor)) + # println(io, Printf.@sprintf("Bundle alpha: %2.4e", dual_solution_regime.bundle_alpha)) + # println(io, Printf.@sprintf("Bundle factor: %2.4e", dual_solution_regime.bundle_factor)) + # end + # println(io, "----------------------------------------------------------------------------------------------------------------------------------------") + #end + println(io, "----------------------------------------------------------------------------------------------------------------------------------------") print(io, "Cut selection used: ") println(io, algo_params.cut_selection_regime) - println(io, "------------------------------------------------------------------------") - print(io, Printf.@sprintf("LP solver: %15s", applied_solvers.LP)) - print(io, Printf.@sprintf("MILP solver: %15s", applied_solvers.MILP)) - print(io, Printf.@sprintf("(MI)NLP solver: %15s", applied_solvers.NLP)) - print(io, Printf.@sprintf("Lagrange solver: %15s", applied_solvers.Lagrange)) - println(io, "------------------------------------------------------------------------") + println(io, "----------------------------------------------------------------------------------------------------------------------------------------") + println(io, Printf.@sprintf("LP solver: %15s", applied_solvers.LP)) + println(io, Printf.@sprintf("MILP solver: %15s", applied_solvers.MILP)) + println(io, Printf.@sprintf("(MI)NLP solver: %15s", applied_solvers.NLP)) + println(io, Printf.@sprintf("Lagrange solver: %15s", applied_solvers.Lagrange)) + println(io, "----------------------------------------------------------------------------------------------------------------------------------------") flush(io) end @@ -199,7 +203,7 @@ end function print_iteration(io, log::Log) print(io, lpad(Printf.@sprintf("%5d", log.iteration), 15)) print(io, " ") - print(io, lpad(Printf.@sprintf("%1.6e", log.upper_bound), 13)) + print(io, lpad(Printf.@sprintf("%1.6e", log.current_upper_bound), 13)) print(io, " ") print(io, lpad(Printf.@sprintf("%1.6e", log.best_upper_bound), 16)) print(io, " ") @@ -267,7 +271,7 @@ function print_footer(io, training_results) flush(io) end -function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log_file_handle::Any, log::DynamicSDDiP.Log) +function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log_file_handle::Any, log::Vector{DynamicSDDiP.Log}) if algo_params.print_level > 0 && mod(length(log), algo_params.log_frequency) == 0 print_helper(print_iteration, log_file_handle, log[end]) end diff --git a/src/regularizations.jl b/src/regularizations.jl index b061cb96..84131c9d 100644 --- a/src/regularizations.jl +++ b/src/regularizations.jl @@ -140,18 +140,19 @@ function regularize_binary!(node::SDDP.Node, node_index::Int64, subproblem::JuMP ############################################################################ # DETERMINE SIGMA TO BE USED IN BINARY SPACE ############################################################################ - Umax = 0 + U_max = 0 for (i, (name, state_comp)) in enumerate(node.states) # TODO: is .out correct here? variable_info = node.ext[:state_info_storage][name].out - if variable_info.upper_bound > Umax - Umax = variable_info.upper_bound + if variable_info.upper_bound > U_max + U_max = variable_info.upper_bound end end + # Here, not sigma, but a different regularization parameter is used - sigma_bin = regularization_regime.sigma[node_index] * Umax + sigma_bin = regularization_regime.sigma[node_index] * U_max ############################################################################ # UNFIX THE STATE VARIABLES diff --git a/src/sigmaTest.jl b/src/sigmaTest.jl index 68eb5935..95a25714 100644 --- a/src/sigmaTest.jl +++ b/src/sigmaTest.jl @@ -28,7 +28,7 @@ function forward_sigma_test( # Storage for the list of outgoing states that we visit on the forward pass. sampled_states = Dict{Symbol,Float64}[] # Our initial incoming state. - incoming_state_value = copy(model.ext[:lin_initial_root_state]) + incoming_state_value = copy(options.initial_state) # A cumulator for the stage-objectives. cumulative_value = 0.0 @@ -58,17 +58,15 @@ function forward_sigma_test( incoming_state_value, # only values, no State struct! noise, scenario_path[1:depth], - algo_params.infiltrate_state, + algo_params, algo_params.regularization_regime, ) end - #@infiltrate - # SOLVE NON-REGULARIZED PROBLEM ######################################################################## # Solve the subproblem, note that `require_duals = false`. - TimerOutputs.@timeit DynamicSDDiP "solve_sigma_test" begin + TimerOutputs.@timeit DynamicSDDiP_TIMER "solve_sigma_test" begin non_reg_results = solve_subproblem_forward( model, node, @@ -76,8 +74,8 @@ function forward_sigma_test( incoming_state_value, # only values, no State struct! noise, scenario_path[1:depth], - algo_params.infiltrate_state, - DynamicSDDiP.NoRegularization, + algo_params, + DynamicSDDiP.NoRegularization(), ) end diff --git a/src/solverHandling.jl b/src/solverHandling.jl index 72f2f32f..3a9405fd 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -29,7 +29,7 @@ function set_solver!( if cut_projection_regime == DynamicSDDiP.KKT || cut_projection_regime == DynamicSDDiP.StrongDuality solver = applied_solvers.MINLP else - solver = applied_solvers.lagrange + solver = applied_solvers.Lagrange end elseif algorithmic_step in [:level_bundle] solver = applied_solvers.NLP @@ -75,7 +75,7 @@ function set_solver!( end - JuMP.unset_silent(subproblem) + JuMP.set_silent(subproblem) return end diff --git a/src/stopping.jl b/src/stopping.jl index e1cc858c..e6bf41fd 100644 --- a/src/stopping.jl +++ b/src/stopping.jl @@ -16,10 +16,10 @@ stopping_rule_status(::DeterministicStopping) = :DeterministicStopping -function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::DeterministicStopping, loop::Symbol) +function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::DeterministicStopping) - bool_rtol = abs(log[end].best_upper_bound - log[end].lower_bound)/abs(max(log[end].best_upper_bound, log[end].lower_bound)) <= log[end].algo_params.opt_rtol - bool_atol = log[end].best_upper_bound - log[end].lower_bound <= log[end].algo_params.opt_atol + bool_rtol = abs(log[end].best_upper_bound - log[end].lower_bound)/abs(max(log[end].best_upper_bound, log[end].lower_bound)) <= rule.rtol + bool_atol = log[end].best_upper_bound - log[end].lower_bound <= rule.atol bool_neg = log[end].best_upper_bound - log[end].lower_bound >= -1e-4 return (bool_rtol || bool_atol) && bool_neg @@ -28,18 +28,14 @@ end # ======================= Iteration Limit Stopping Rule ====================== # stopping_rule_status(::SDDP.IterationLimit) = :iteration_limit -function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::SDDP.IterationLimit, loop::Symbol) - if loop == :inner - return false - elseif loop == :outer - return log[end].outer_iteration >= rule.limit - end +function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::SDDP.IterationLimit) + return log[end].iteration >= rule.limit end # ========================= Time Limit Stopping Rule ========================= # stopping_rule_status(::SDDP.TimeLimit) = :time_limit -function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::SDDP.TimeLimit, loop::Symbol) +function convergence_test(graph::SDDP.PolicyGraph, log::Vector{Log}, rule::SDDP.TimeLimit) return log[end].time >= rule.limit end @@ -48,10 +44,9 @@ function convergence_test( graph::SDDP.PolicyGraph, log::Vector{Log}, stopping_rules::Vector{SDDP.AbstractStoppingRule}, - loop::Symbol ) for stopping_rule in stopping_rules - if convergence_test(graph, log, stopping_rule, loop) + if convergence_test(graph, log, stopping_rule) return true, stopping_rule_status(stopping_rule) end end diff --git a/src/typedefs.jl b/src/typedefs.jl index fc9faf66..b0a594ba 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -357,7 +357,7 @@ mutable struct AlgoParams cut_type = SDDP.SINGLE_CUT, refine_at_similar_nodes = true, cycle_discretization_delta = 0.0, - print_level = 1, + print_level = 2, log_frequency = 1, log_file = "DynamicSDDiP.log", run_numerical_stability_report = true, From a4a6f72e46c4441f8441a49a5361030cd17bcb01 Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 22 Oct 2021 17:48:13 -0400 Subject: [PATCH 29/30] Further corrections due to errors --- src/algorithmMain.jl | 10 +++--- src/bellman.jl | 75 ++++++++++++++++++++++++++++--------------- src/duals.jl | 12 +++++-- src/lagrange.jl | 40 +++++++++++++++-------- src/logging.jl | 15 +++++++-- src/solverHandling.jl | 14 ++++++-- src/typedefs.jl | 17 +++++++--- 7 files changed, 127 insertions(+), 56 deletions(-) diff --git a/src/algorithmMain.jl b/src/algorithmMain.jl index 837ad719..52afcff8 100644 --- a/src/algorithmMain.jl +++ b/src/algorithmMain.jl @@ -159,7 +159,7 @@ function solve( finally end - @infiltrate + #@infiltrate ############################################################################ # lOG MODEL RESULTS @@ -231,7 +231,7 @@ function solve_DynamicSDDiP(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGrap ############################################################################ TimerOutputs.@timeit DynamicSDDiP_TIMER "loop" begin status = master_loop(parallel_scheme, model, options, algo_params, - applied_solvers, algo_params.regularization_regime) + applied_solvers) end return status @@ -244,8 +244,7 @@ Loop function of DynamicSDDiP. function master_loop(parallel_scheme::SDDP.Serial, model::SDDP.PolicyGraph{T}, options::DynamicSDDiP.Options, algo_params::DynamicSDDiP.AlgoParams, - applied_solvers::DynamicSDDiP.AppliedSolvers, - regularization_regime::DynamicSDDiP.Regularization) where {T} + applied_solvers::DynamicSDDiP.AppliedSolvers) where {T} ############################################################################ # INITIALIZE PARAMETERS REQUIRED FOR REFINEMENTS @@ -474,6 +473,8 @@ function iteration( forward_trajectory = DynamicSDDiP.forward_pass(model, options, algo_params, applied_solvers, algo_params.forward_pass) end + #@infiltrate + ############################################################################ # BINARY REFINEMENT ############################################################################ @@ -522,6 +523,7 @@ function iteration( ############################################################################ # CALCULATE LOWER BOUND ############################################################################ + #@infiltrate TimerOutputs.@timeit DynamicSDDiP_TIMER "calculate_bound" begin first_stage_results = calculate_bound(model) end diff --git a/src/bellman.jl b/src/bellman.jl index b78d641b..b7db67e2 100644 --- a/src/bellman.jl +++ b/src/bellman.jl @@ -268,7 +268,11 @@ function _add_average_cut( ############################################################################ # As cuts are created for the value function of the following state, # we need the parameters for this stage. - sigma = algo_params.regularization_regime.sigma[node_index+1] + if isa(algo_params.regularization_regime, DynamicSDDiP.NoRegularization) + sigma = nothing + else + sigma = algo_params.regularization_regime.sigma[node_index+1] + end ############################################################################ # ADD THE CUT USING THE NEW EXPECTED COEFFICIENTS @@ -309,7 +313,7 @@ function _add_cut( xᵏ::Dict{Symbol,Float64}, # trial point (anchor point for cut without BinaryApproximation), outgoing_state # obj_y::Union{Nothing,NTuple{N,Float64}}, # belief_y::Union{Nothing,Dict{T,Float64}}, - sigma::Float64, + sigma::Union{Nothing,Float64}, iteration::Int64, infiltrate_state::Symbol, algo_params::DynamicSDDiP.AlgoParams, @@ -325,6 +329,12 @@ function _add_cut( end @infiltrate infiltrate_state in [:bellman, :all] + if isnothing(sigma) + sigma_use = nothing + else + sigma_use = copy(sigma) + end + ############################################################################ # CONSTRUCT NONLINEAR CUT STRUCT ############################################################################ @@ -335,7 +345,7 @@ function _add_cut( xᵏ_b, λᵏ, copy(state_approximation_regime.binary_precision), - copy(sigma), + sigma_use, JuMP.VariableRef[], JuMP.ConstraintRef[], # obj_y, @@ -526,7 +536,7 @@ function represent_cut_projection_closure!( ######################################################## coefficients::Dict{Symbol,Float64}, binary_state::Dict{Symbol,BinaryState}, - sigma::Float64, + sigma::Union{Nothing,Float64}, cut_variables::Vector{JuMP.VariableRef}, cut_constraints::Vector{JuMP.ConstraintRef}, iteration::Int64, @@ -648,7 +658,7 @@ function add_complementarity_constraints!( ######################################################################## cut_variables::Vector{JuMP.VariableRef}, cut_constraints::Vector{JuMP.ConstraintRef}, - sigma::Float64, + sigma::Union{Nothing,Float64}, iteration::Int64, beta::Float64, ######################################################################## @@ -700,7 +710,7 @@ function add_complementarity_constraints!( ######################################################################## cut_variables::Vector{JuMP.VariableRef}, cut_constraints::Vector{JuMP.ConstraintRef}, - sigma::Float64, + sigma::Union{Nothing,Float64}, iteration::Int64, beta::Float64, ######################################################################## @@ -773,7 +783,7 @@ end Determine a reasonable bigM value based on the maximum upper bound of all state components, sigma and beta. This could be improved later. """ -function get_bigM(node::SDDP.Node, sigma::Float64, beta::Float64, related_coefficients::Vector{Float64}, K::Int64) +function get_bigM(node::SDDP.Node, sigma::Union{Nothing,Float64}, beta::Float64, related_coefficients::Vector{Float64}, K::Int64) ############################################################################ # DETERMINE U_MAX @@ -799,13 +809,18 @@ function get_bigM(node::SDDP.Node, sigma::Float64, beta::Float64, related_coeffi # DETERMINE BIG-M ############################################################################ bigM = 0 - for k in 1:K - candidate = U_max * (sigma + abs(related_coefficients[k]) / (2^(k-1) * beta)) - if bigM < candidate - bigM = candidate + if isnothing(sigma) + # no regularization is used, so bigM is just bounded by an arbitrary value + bigM = 1e4 + else + for k in 1:K + candidate = U_max * (sigma + abs(related_coefficients[k]) / (2^(k-1) * beta)) + if bigM < candidate + bigM = candidate + end end + # bigM = sigma end - # bigM = sigma return bigM end @@ -822,7 +837,7 @@ function add_complementarity_constraints!( ######################################################################## cut_variables::Vector{JuMP.VariableRef}, cut_constraints::Vector{JuMP.ConstraintRef}, - sigma::Float64, + sigma::Union{Nothing,Float64}, iteration::Int64, beta::Float64, ######################################################################## @@ -845,20 +860,20 @@ function add_complementarity_constraints!( # AUXILIARY VARIABLE ############################################################################ # First represent γ[k]-1 as a new variable - ρ = JuMP.@variable(model, [k in 1:K], lower_bound=-1, upper__bound=0, base_name = "ρ_" * string(state_index) * "_it" * string(iteration)) + ρ = JuMP.@variable(model, [k in 1:K], lower_bound=0, upper_bound=1, base_name = "ρ_" * string(state_index) * "_it" * string(iteration)) append!(cut_variables, ρ) - ρ_constraint = JuMP.@constraint(model, [k in 1:K], ρ[k] == γ[k] - 1) + ρ_constraint = JuMP.@constraint(model, [k in 1:K], ρ[k] == 1 - γ[k]) append!(cut_constraints, ρ_constraint) ############################################################################ # ADD SOS1 CONSTRAINTS ############################################################################ for k in 1:K - SOS1_constraint_1 = JuMP.@constraint(model, [γ[k], ν[k]] in SOS1()) - SOS1_constraint_2 = JuMP.@constraint(model, [μ[k], ρ[k]] in SOS1()) - append!(cut_constraints, SOS1_constraint_1) - append!(cut_constraints, SOS1_constraint_2) + SOS1_constraint_1 = JuMP.@constraint(model, [γ[k], ν[k]] in JuMP.SOS1()) + SOS1_constraint_2 = JuMP.@constraint(model, [μ[k], ρ[k]] in JuMP.SOS1()) + push!(cut_constraints, SOS1_constraint_1) + push!(cut_constraints, SOS1_constraint_2) end return @@ -880,7 +895,7 @@ function represent_cut_projection_closure!( ######################################################## coefficients::Dict{Symbol,Float64}, binary_state::Dict{Symbol,BinaryState}, - sigma::Float64, + sigma::Union{Nothing,Float64}, cut_variables::Vector{JuMP.VariableRef}, cut_constraints::Vector{JuMP.ConstraintRef}, iteration::Int64, @@ -910,6 +925,7 @@ function represent_cut_projection_closure!( related_coefficients[index] = coefficients[name] end end + append!(all_coefficients, related_coefficients) ############################################################################ # ADD REQUIRED VARIABLES (CPC-CONSTRAINTS 4, 5) @@ -1007,8 +1023,9 @@ function validity_checks!( == size(all_coefficients, 1) == size(all_mu, 1) ) + @assert (number_of_states == size(all_eta, 1) - == size(V.states, 1) + == length(V.states) ) return @@ -1050,8 +1067,8 @@ function get_cut_expression( expr = JuMP.@expression( model, - V.theta - sum(all_mu[j] for j in 1:number_of_duals) - - sum(x * all_eta[i] for (i, x) in V.states) + V.theta - sum(all_mu[j] for j in 1:size(all_mu, 1)) + - sum(x * all_eta[i] for (i, (_,x)) in enumerate(V.states)) ) return expr @@ -1075,7 +1092,7 @@ function add_strong_duality_cut!( model, sum(all_coefficients[j] * all_lambda[j] for j in 1:number_of_duals) - sum(all_mu[j] for j in 1:number_of_duals) - - sum(x * all_eta[i] for (i, x) in V.states) + - sum(x * all_eta[i] for (i, (_, x)) in enumerate(V.states)) ) constraint_ref = if JuMP.objective_sense(model) == MOI.MIN_SENSE @@ -1121,7 +1138,7 @@ function _add_cut( xᵏ::Dict{Symbol,Float64}, # trial point (anchor point for cut without BinaryApproximation), outgoing_state # obj_y::Union{Nothing,NTuple{N,Float64}}, # belief_y::Union{Nothing,Dict{T,Float64}}, - sigma::Float64, + sigma::Union{Nothing,Float64}, iteration::Int64, infiltrate_state::Symbol, algo_params::DynamicSDDiP.AlgoParams, @@ -1137,6 +1154,12 @@ function _add_cut( end @infiltrate infiltrate_state in [:bellman, :all] + if isnothing(sigma) + sigma_use = nothing + else + sigma_use = copy(sigma) + end + ############################################################################ # CONSTRUCT NONLINEAR CUT STRUCT ############################################################################ @@ -1144,7 +1167,7 @@ function _add_cut( θᵏ, πᵏ, xᵏ, - copy(sigma), + sigma_use, JuMP.ConstraintRef, # obj_y, # belief_y, diff --git a/src/duals.jl b/src/duals.jl index ba7dccce..da61b6a0 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -381,7 +381,13 @@ function get_norm_bound( B_norm_bound = variable_info.upper_bound end end - dual_bound = algo_params.regularization_regime.sigma[node_index] * B_norm_bound + + if isa(algo_params.regularization_regime, DynamicSDDiP.NoRegularization) + # if no regularization is used, bounds should be Inf even if intended to use + dual_bound = Inf + else + dual_bound = algo_params.regularization_regime.sigma[node_index] * B_norm_bound + end return dual_bound end @@ -482,7 +488,7 @@ function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Flo for (i, name) in enumerate(keys(node.ext[:backward_data][:bin_states])) variable_name = node.ext[:backward_data][:bin_states][name] reference_to_constr = FixRef(variable_name) - dual_vars_initial[i] = JuMP.getdual(reference_to_constr) + dual_vars_initial[i] = JuMP.dual(reference_to_constr) end return @@ -493,7 +499,7 @@ function get_and_set_dual_values!(node::SDDP.Node, dual_vars_initial::Vector{Flo for (i, name) in enumerate(keys(node,states)) reference_to_constr = FixRef(name.in) - dual_vars_initial[i] = JuMP.getdual(reference_to_constr) + dual_vars_initial[i] = JuMP.dual(reference_to_constr) end return diff --git a/src/lagrange.jl b/src/lagrange.jl index 0967b822..eee3e313 100644 --- a/src/lagrange.jl +++ b/src/lagrange.jl @@ -175,7 +175,10 @@ function solve_lagrangian_dual( # SOLVE LAGRANGIAN RELAXATION FOR GIVEN DUAL_VARS ######################################################################## # Evaluate the inner problem and determine a subgradient - L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) + TimerOutputs.@timeit DynamicSDDiP_TIMER "Lagrange_inner" begin + L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) + end + @infiltrate algo_params.infiltrate_state in [:all, :lagrange] ######################################################################## @@ -195,7 +198,9 @@ function solve_lagrangian_dual( # SOLVE APPROXIMATION MODEL ######################################################################## # Get a bound from the approximate model - JuMP.optimize!(approx_model) + TimerOutputs.@timeit DynamicSDDiP_TIMER "Lagrange_outer" begin + JuMP.optimize!(approx_model) + end @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL t_k = JuMP.objective_value(approx_model) π_k .= JuMP.value.(π) @@ -411,6 +416,7 @@ function magnanti_wong!( return end JuMP.@constraint(approx_model, t <= s * (L_k + h_k' * (π .- π_k))) + end return @@ -492,7 +498,8 @@ function solve_lagrangian_dual( ############################################################################ # RELAXING THE COPY CONSTRAINTS ############################################################################ - relax_copy_constraints(node, x_in_value, h_expr, algo_params.state_approximation_regime) + relax_copy_constraints!(node, x_in_value, h_expr, algo_params.state_approximation_regime) + node.ext[:backward_data][:old_rhs] = x_in_value ############################################################################ # LOGGING OF LAGRANGIAN DUAL @@ -505,7 +512,7 @@ function solve_lagrangian_dual( ############################################################################ # Approximation of Lagrangian dual by cutting planes # Optimizer is re-set anyway - approx_model = JuMP.Model(GLPK.Optimizer) + approx_model = JuMP.Model(Gurobi.Optimizer) # Create the objective # Note that it is always formulated as a maximization problem, but that @@ -523,13 +530,13 @@ function solve_lagrangian_dual( set_multiplier_bounds!(approx_model, number_of_states, bound_results.dual_bound) ############################################################################ - # CUTTING-PLANE METHOD + # BUNDLE METHOD ############################################################################ iter = 0 lag_status = :none # set up optimal value of approx_model (former f_approx) - t_k = -Inf + t_k = 0 while iter <= iteration_limit && !isapprox(L_star, t_k, atol = atol, rtol = rtol) iter += 1 @@ -538,7 +545,9 @@ function solve_lagrangian_dual( # SOLVE LAGRANGIAN RELAXATION FOR GIVEN DUAL_VARS ######################################################################## # Evaluate the inner problem and determine a subgradient - L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) + TimerOutputs.@timeit DynamicSDDiP_TIMER "Lagrange_inner" begin + L_k = _solve_Lagrangian_relaxation!(node, π_k, h_expr, h_k, true) + end @infiltrate algo_params.infiltrate_state in [:all, :lagrange] ######################################################################## @@ -564,7 +573,9 @@ function solve_lagrangian_dual( # SOLVE APPROXIMATION MODEL ######################################################################## # Get a bound from the approximate model - JuMP.optimize!(approx_model) + TimerOutputs.@timeit DynamicSDDiP_TIMER "Lagrange_outer" begin + JuMP.optimize!(approx_model) + end @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL t_k = JuMP.objective_value(approx_model) @infiltrate algo_params.infiltrate_state in [:all, :lagrange] @@ -633,7 +644,7 @@ function solve_lagrangian_dual( level = f_up - gap * level_factor # - atol/10.0 for numerical issues? - JuMP.setlowerbound(t, level) + JuMP.set_lower_bound(t, level) ######################################################################## # DETERMINE NEXT ITERATION USING PROXIMAL PROBLEM @@ -642,9 +653,12 @@ function solve_lagrangian_dual( # TODO: Does this work with π[i]? JuMP.@objective(approx_model, Min, sum((π_k[i] - π[i])^2 for i in 1:number_of_states)) set_solver!(approx_model, algo_params, applied_solvers, :level_bundle) - JuMP.optimize!(approx_model) + TimerOutputs.@timeit DynamicSDDiP_TIMER "Lagrange_Bundle" begin + JuMP.optimize!(approx_model) + end + @assert JuMP.termination_status(approx_model) == JuMP.MOI.OPTIMAL - π_k .= JuMP.value(π) + π_k .= JuMP.value.(π) ######################################################################## if L_star > t_k + atol/10.0 @@ -679,8 +693,8 @@ function solve_lagrangian_dual( ############################################################################ # APPLY MAGNANTI AND WONG APPROACH IF INTENDED ############################################################################ - magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_k, L_star, - iteration_limit, atol, rtol, algo_params.dual_choice_regime, iter) + magnanti_wong!(node, approx_model, π_k, π_star, t_k, h_expr, h_k, s, L_star, + iteration_limit, atol, rtol, algo_params.duality_regime.dual_choice_regime, iter) ############################################################################ # RESTORE THE COPY CONSTRAINT x.in = value(x.in) (̄x = z) diff --git a/src/logging.jl b/src/logging.jl index f8625101..bf0fae6d 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -195,12 +195,12 @@ end function print_iteration_header(io) println( io, - " Inner_Iteration Upper Bound Best Upper Bound Lower Bound Gap Time (s) sigma_ref bin_ref tot_var bin_var int_var con cuts active Lag iterations & status ", + " Inner_Iteration Upper Bound Best Upper Bound Lower Bound Gap Time (s) Time_it (s) sigma_ref bin_ref tot_var bin_var int_var con cuts active Lag iterations & status ", ) flush(io) end -function print_iteration(io, log::Log) +function print_iteration(io, log::Log, start_time::Float64) print(io, lpad(Printf.@sprintf("%5d", log.iteration), 15)) print(io, " ") print(io, lpad(Printf.@sprintf("%1.6e", log.current_upper_bound), 13)) @@ -215,6 +215,8 @@ function print_iteration(io, log::Log) print(io, " ") print(io, lpad(Printf.@sprintf("%1.6e", log.time), 13)) print(io, " ") + print(io, lpad(Printf.@sprintf("%1.6e", log.time - start_time), 13)) + print(io, " ") if !isnothing(log.sigma_increased) print(io, Printf.@sprintf("%9s", log.sigma_increased ? "true" : "false")) else @@ -273,7 +275,14 @@ end function log_iteration(algo_params::DynamicSDDiP.AlgoParams, log_file_handle::Any, log::Vector{DynamicSDDiP.Log}) if algo_params.print_level > 0 && mod(length(log), algo_params.log_frequency) == 0 - print_helper(print_iteration, log_file_handle, log[end]) + # Get time() after last iteration to compute iteration specific time + if lastindex(log) > 1 + start_time = log[end-1].time + else + start_time = 0.0 + end + + print_helper(print_iteration, log_file_handle, log[end], start_time) end end diff --git a/src/solverHandling.jl b/src/solverHandling.jl index 3a9405fd..2d2e83c0 100644 --- a/src/solverHandling.jl +++ b/src/solverHandling.jl @@ -20,14 +20,18 @@ function set_solver!( # CHOOSE THE CORRECT TYPE OF SOLVER AND SOLVER ############################################################################ if algorithmic_step in [:forward_pass, :backward_pass] - if cut_projection_regime == DynamicSDDiP.KKT || cut_projection_regime == DynamicSDDiP.StrongDuality + if isa(cut_projection_regime, DynamicSDDiP.KKT) solver = applied_solvers.MINLP + elseif isa(cut_projection_regime, DynamicSDDiP.StrongDuality) + solver = applied_solvers.MIQCP else solver = applied_solvers.MILP end elseif algorithmic_step in [:lagrange_relax] - if cut_projection_regime == DynamicSDDiP.KKT || cut_projection_regime == DynamicSDDiP.StrongDuality + if isa(cut_projection_regime, DynamicSDDiP.KKT) solver = applied_solvers.MINLP + elseif isa(cut_projection_regime, DynamicSDDiP.StrongDuality) + solver = applied_solvers.MIQCP else solver = applied_solvers.Lagrange end @@ -75,7 +79,11 @@ function set_solver!( end - JuMP.set_silent(subproblem) + if algo_params.silent + JuMP.set_silent(subproblem) + else + JuMP.unset_silent(subproblem) + end return end diff --git a/src/typedefs.jl b/src/typedefs.jl index b0a594ba..b6fe69c5 100644 --- a/src/typedefs.jl +++ b/src/typedefs.jl @@ -93,7 +93,7 @@ mutable struct LevelBundle <: AbstractDualSolutionRegime # atol = 1e-8, # rtol = 1e-8, # iteration_limit = 1000, - level_factor = 1.0, + level_factor = 0.5, ) #return new(atol, rtol, iteration_limit, level_factor) return new(level_factor) @@ -240,7 +240,10 @@ Regularization means that in the forward pass some regularized value functions stopping criterion is satisfied. Furthermore, it can be exploited in combination with bounding the dual variables. NoRegularization means that no regularization is used. This may be detrimental - w.r.t. convergence. + w.r.t. convergence. Note that it also makes it difficult to bound the + dual multipliers and the bigM parameters appropriately. + For bigM so far 1e4 is used. For dual multipliers no bound is applied + even if BothBounds is chosen. Default is Regularization. """ @@ -341,6 +344,7 @@ mutable struct AlgoParams log_file::String run_numerical_stability_report::Bool numerical_focus::Bool + silent::Bool infiltrate_state::Symbol function AlgoParams(; @@ -362,6 +366,7 @@ mutable struct AlgoParams log_file = "DynamicSDDiP.log", run_numerical_stability_report = true, numerical_focus = false, + silent = true, infiltrate_state = :none, ) return new( @@ -383,6 +388,7 @@ mutable struct AlgoParams log_file, run_numerical_stability_report, numerical_focus, + silent, infiltrate_state, ) end @@ -399,6 +405,7 @@ For the Lagrangian subproblems a separate solver can be defined if for struct AppliedSolvers LP :: Any MILP :: Any + MIQCP :: Any MINLP :: Any NLP :: Any Lagrange :: Any @@ -406,13 +413,15 @@ struct AppliedSolvers function AppliedSolvers(; LP = "Gurobi", MILP = "Gurobi", - MINLP = "Gurobi", + MIQCP = "Gurobi", + MINLP = "SCIP", NLP = "SCIP", Lagrange = "Gurobi", ) return new( LP, MILP, + MIQCP, MINLP, NLP, Lagrange @@ -435,7 +444,7 @@ mutable struct NonlinearCut <: Cut binary_state::Dict{Symbol,DynamicSDDiP.BinaryState} binary_precision::Dict{Symbol,Float64} ############################################################################ - sigma::Float64 + sigma::Union{Nothing,Float64} ############################################################################ cut_variables::Vector{JuMP.VariableRef} cut_constraints::Vector{JuMP.ConstraintRef} From 17712484b86b225ca2d9b839e41731cc52cb894e Mon Sep 17 00:00:00 2001 From: ChrisFuelOR <72441442+ChrisFuelOR@users.noreply.github.com> Date: Fri, 22 Oct 2021 17:48:26 -0400 Subject: [PATCH 30/30] Added and changed some examples --- examples/newExample_1.jl | 13 +- examples/newExample_2.jl | 174 ++++++++++++++++++++++ examples/unitCommitment.jl | 289 +++++++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+), 4 deletions(-) create mode 100644 examples/newExample_2.jl create mode 100644 examples/unitCommitment.jl diff --git a/examples/newExample_1.jl b/examples/newExample_1.jl index 0be31f0b..23c00181 100644 --- a/examples/newExample_1.jl +++ b/examples/newExample_1.jl @@ -21,7 +21,7 @@ function model_config() # Duality / Cut computation configuration dual_initialization_regime = DynamicSDDiP.ZeroDuals() - dual_solution_regime = DynamicSDDiP.Kelley() + dual_solution_regime = DynamicSDDiP.LevelBundle() dual_bound_regime = DynamicSDDiP.BothBounds() dual_status_regime = DynamicSDDiP.Rigorous() dual_choice_regime = DynamicSDDiP.MagnantiWongChoice() @@ -37,7 +37,7 @@ function model_config() ) # State approximation and cut projection configuration - cut_projection_regime = DynamicSDDiP.SOS1() + cut_projection_regime = DynamicSDDiP.BigM() binary_precision = Dict{Symbol, Float64}() state_approximation_regime = DynamicSDDiP.BinaryApproximation( @@ -53,6 +53,9 @@ function model_config() # File for logging log_file = "C:/Users/cg4102/Documents/julia_logs/newExample_1.log" + # Suppress solver output + silent = true + # Infiltration for debugging infiltrate_state = :none @@ -64,6 +67,7 @@ function model_config() duality_regime = duality_regime, cut_selection_regime = cut_selection_regime, log_file = log_file, + silent = silent, infiltrate_state = infiltrate_state, ) @@ -71,8 +75,9 @@ function model_config() applied_solvers = DynamicSDDiP.AppliedSolvers( LP = "Gurobi", MILP = "Gurobi", - MINLP = "Gurobi", - NLP = "SCIP", + MIQCP = "Gurobi", + MINLP = "SCIP", + NLP = "Gurobi", Lagrange = "Gurobi", ) diff --git a/examples/newExample_2.jl b/examples/newExample_2.jl new file mode 100644 index 00000000..11e76bc8 --- /dev/null +++ b/examples/newExample_2.jl @@ -0,0 +1,174 @@ +# Copyright (c) 2021 Christian Fuellner + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +using JuMP +using SDDP +using DynamicSDDiP +using Revise +using Gurobi +using GAMS +#using SCIP +using Infiltrator + + +function model_config() + + # Stopping rules to be used + stopping_rules = [DynamicSDDiP.DeterministicStopping()] + + # Duality / Cut computation configuration + dual_initialization_regime = DynamicSDDiP.ZeroDuals() + dual_solution_regime = DynamicSDDiP.Kelley() + dual_bound_regime = DynamicSDDiP.BothBounds() + dual_status_regime = DynamicSDDiP.Rigorous() + dual_choice_regime = DynamicSDDiP.MagnantiWongChoice() + duality_regime = DynamicSDDiP.LagrangianDuality( + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, + dual_initialization_regime = dual_initialization_regime, + dual_bound_regime = dual_bound_regime, + dual_solution_regime = dual_solution_regime, + dual_choice_regime = dual_choice_regime, + dual_status_regime = dual_status_regime, + ) + + # State approximation and cut projection configuration + cut_projection_regime = DynamicSDDiP.BigM() + binary_precision = Dict{Symbol, Float64}() + + state_approximation_regime = DynamicSDDiP.BinaryApproximation( + binary_precision = binary_precision, + cut_projection_regime = cut_projection_regime) + + # Regularization configuration + regularization_regime = DynamicSDDiP.Regularization(sigma = [0.0, 1.0], sigma_factor = 5.0) + # regularization_regime = DynamicSDDiP.NoRegularization() + + # Cut selection configuration + cut_selection_regime = DynamicSDDiP.NoCutSelection() + + # File for logging + log_file = "C:/Users/cg4102/Documents/julia_logs/newExample_2.log" + + # Suppress solver output + silent = true + + # Infiltration for debugging + infiltrate_state = :none + + # Definition of algo_params + algo_params = DynamicSDDiP.AlgoParams( + stopping_rules = stopping_rules, + state_approximation_regime = state_approximation_regime, + regularization_regime = regularization_regime, + duality_regime = duality_regime, + cut_selection_regime = cut_selection_regime, + log_file = log_file, + silent = silent, + infiltrate_state = infiltrate_state, + ) + + # Define solvers to be used + applied_solvers = DynamicSDDiP.AppliedSolvers( + LP = "Gurobi", + MILP = "Gurobi", + MIQCP = "Gurobi", + MINLP = "SCIP", + NLP = "Gurobi", + Lagrange = "Gurobi", + ) + + # Start model with used configuration + model_starter( + algo_params, + applied_solvers, + ) +end + + +function model_starter( + algo_params::DynamicSDDiP.AlgoParams = DynamicSDDiP.AlgoParams(), + applied_solvers::DynamicSDDiP.AppliedSolvers = DynamicSDDiP.AppliedSolvers(), + ) + + ############################################################################ + # DEFINE MODEL + ############################################################################ + model = model_definition() + + ############################################################################ + # DEFINE BINARY APPROXIMATION IF INTENDED + ############################################################################ + # for (name, state_comp) in model.nodes[1].ext[:lin_states] + # ub = JuMP.upper_bound(state_comp.out) + # + # string_name = string(name) + # if occursin("gen", string_name) + # binaryPrecision[name] = binaryPrecisionFactor * ub + # else + # binaryPrecision[name] = 1 + # end + # end + + ############################################################################ + # SOLVE MODEL + ############################################################################ + DynamicSDDiP.solve(model, algo_params, applied_solvers) +end + + +function model_definition() + + number_of_stages = 2 + + model = SDDP.LinearPolicyGraph( + stages = number_of_stages, + lower_bound = 0.0, + optimizer = GAMS.Optimizer, + sense = :Min + ) do subproblem, t + + ######################################################################## + # DEFINE STAGE-t MODEL + ######################################################################## + # State variables + JuMP.@variable(subproblem, 0.0 <= x <= 1.0, SDDP.State, initial_value = 0) + + if t == 1 + + # Constraints + x = subproblem[:x] + JuMP.@variable(subproblem, v) + JuMP.@constraint(subproblem, v >= 0.7 - 6/5*x.out) + JuMP.@constraint(subproblem, v >= -1.1 + 2.4*x.out) + + # Stage objective + SDDP.@stageobjective(subproblem, v) + + else + # Local variables + JuMP.@variable(subproblem, 0.0 <= y[i=1:2]) + JuMP.set_integer(y[1]) + JuMP.set_upper_bound(y[1], 2) + JuMP.set_upper_bound(y[2], 3) + + # Constraints + x = subproblem[:x] + JuMP.@constraint(subproblem, x.out == 0) + JuMP.@constraint(subproblem, con, 2*y[1] + y[2] >= 3*x.in) + + # Stage objective + SDDP.@stageobjective(subproblem, y[1] + y[2]) + + end + + end + + return model +end + +model_config() diff --git a/examples/unitCommitment.jl b/examples/unitCommitment.jl new file mode 100644 index 00000000..a28b7f7e --- /dev/null +++ b/examples/unitCommitment.jl @@ -0,0 +1,289 @@ +# Copyright (c) 2021 Christian Fuellner + +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +################################################################################ + +using JuMP +using SDDP +using DynamicSDDiP +using Revise +using Gurobi +using GAMS +#using SCIP +using Infiltrator + + +function model_config() + + # Stopping rules to be used + stopping_rules = [DynamicSDDiP.DeterministicStopping()] + # iteration limit 50? time limit 36000? + + # Duality / Cut computation configuration + dual_initialization_regime = DynamicSDDiP.ZeroDuals() + dual_solution_regime = DynamicSDDiP.Kelley() + dual_bound_regime = DynamicSDDiP.BothBounds() + dual_status_regime = DynamicSDDiP.Rigorous() + dual_choice_regime = DynamicSDDiP.MagnantiWongChoice() + duality_regime = DynamicSDDiP.LagrangianDuality( + atol = 1e-8, + rtol = 1e-8, + iteration_limit = 1000, + dual_initialization_regime = dual_initialization_regime, + dual_bound_regime = dual_bound_regime, + dual_solution_regime = dual_solution_regime, + dual_choice_regime = dual_choice_regime, + dual_status_regime = dual_status_regime, + ) + + # State approximation and cut projection configuration + cut_projection_regime = DynamicSDDiP.BigM() + binary_precision = Dict{Symbol, Float64}() + + state_approximation_regime = DynamicSDDiP.BinaryApproximation( + binary_precision = binary_precision, + cut_projection_regime = cut_projection_regime) + + # Regularization configuration + regularization_regime = DynamicSDDiP.Regularization(sigma = [0.0, 1.0], sigma_factor = 5.0) + + # Cut selection configuration + cut_selection_regime = DynamicSDDiP.NoCutSelection() + + # File for logging + log_file = "C:/Users/cg4102/Documents/julia_logs/unitCommitment.log" + + # Suppress solver output + silent = true + + # Infiltration for debugging + infiltrate_state = :none + + # Definition of algo_params + algo_params = DynamicSDDiP.AlgoParams( + stopping_rules = stopping_rules, + state_approximation_regime = state_approximation_regime, + regularization_regime = regularization_regime, + duality_regime = duality_regime, + cut_selection_regime = cut_selection_regime, + log_file = log_file, + silent = silent, + infiltrate_state = infiltrate_state, + ) + + # Define solvers to be used + applied_solvers = DynamicSDDiP.AppliedSolvers( + LP = "Gurobi", + MILP = "Gurobi", + MIQCP = "Gurobi", + MINLP = "SCIP", + NLP = "Gurobi", + Lagrange = "Gurobi", + ) + + # Start model with used configuration + model_starter( + algo_params, + applied_solvers, + ) +end + + +function model_starter( + algo_params::DynamicSDDiP.AlgoParams = DynamicSDDiP.AlgoParams(), + applied_solvers::DynamicSDDiP.AppliedSolvers = DynamicSDDiP.AppliedSolvers(), + ) + + ############################################################################ + # DEFINE MODEL + ############################################################################ + model = model_definition() + + ############################################################################ + # DEFINE BINARY APPROXIMATION IF INTENDED + ############################################################################ + # for (name, state_comp) in model.nodes[1].ext[:lin_states] + # ub = JuMP.upper_bound(state_comp.out) + # + # string_name = string(name) + # if occursin("gen", string_name) + # binaryPrecision[name] = binaryPrecisionFactor * ub + # else + # binaryPrecision[name] = 1 + # end + # end + + ############################################################################ + # SOLVE MODEL + ############################################################################ + DynamicSDDiP.solve(model, algo_params, applied_solvers) +end + + +function model_definition() + + struct Generator + comm_ini::Int + gen_ini::Float64 + pmax::Float64 + pmin::Float64 + fuel_cost::Float64 + om_cost::Float64 + su_cost::Float64 + sd_cost::Float64 + ramp_up::Float64 + ramp_dw::Float64 + end + + struct Storage + level_max::Float64 + level_ini::Float64 + level_end::Float64 + gen_max::Float64 + pump_max::Float64 + gen_eff::Float64 + pump_eff::Float64 + end + + generators = [ + Generator(0, 0.0, 1.18, 0.32, 48.9, 0.0, 182.35, 18.0, 0.42, 0.33), + Generator(1, 1.06, 1.19, 0.37, 52.1, 0.0, 177.68, 17.0, 0.31, 0.36), + Generator(0, 0.0, 1.05, 0.48, 42.8, 0.0, 171.69, 17.0, 0.21, 0.22), + Generator(0, 0.0, 1.13, 0.48, 54.0, 0.0, 171.60, 17.0, 0.28, 0.27), + Generator(0, 0.0, 1.02, 0.47, 49.4, 0.0, 168.04, 17.0, 0.22, 0.275), + Generator(1, 0.72, 1.9, 0.5, 64.1, 0.0, 289.59, 28.0, 0.52, 0.62), + Generator(0, 0.0, 2.08, 0.62, 60.3, 0.0, 286.89, 28.0, 0.67, 0.5), + Generator(1, 0.55, 2.11, 0.55, 66.1, 0.0, 329.89, 33.0, 0.64, 0.69), + Generator(1, 2.2, 2.82, 0.85, 61.6, 0.0, 486.81, 49.0, 0.9, 0.79), + Generator(0, 0.0, 3.23, 0.84, 54.9, 0.0, 503.34, 50.0, 1.01, 1.00), + ] + + demand_penalty = 5e2 + demand = [3.06 2.91 2.71 2.7 2.73 2.91 3.38 4.01 4.6 4.78 4.81 4.84 4.89 4.44 4.57 4.6 4.58 4.47 4.32 4.36 4.5 4.27 3.93 3.61 3.43 3.02 2.9 2.54 2.73 3.01 3.45 3.89 4.5 4.76 4.9 5.04] + + storages = [ + Storage(1.2, 0.5, 0.7, 0.45, 0.4, 0.9, 0.85), + Storage(0.8, 0.3, 0.25, 0.35, 0.3, 0.92, 0.87), + ] + + inflow = [0.2 0.3 0.4; 0.1 0.05 0.1] + + + number_of_generators = 5 + number_of_storages = 2 + number_of_stages = 2 + + model = SDDP.LinearPolicyGraph( + stages = number_of_stages, + lower_bound = 0.0, + optimizer = GAMS.Optimizer, + sense = :Min + ) do subproblem, t + + ######################################################################## + # DEFINE STAGE-t MODEL + ######################################################################## + # State variables + JuMP.@variable( + subproblem, + 0.0 <= commit[i = 1:num_of_generators] <= 1.0, + SDDP.State, + Bin, + initial_value = generators[i].comm_ini + ) + + JuMP.@variable( + subproblem, + 0.0 <= gen[i = 1:num_of_generators] <= generators[i].pmax, + SDDP.State, + initial_value = generators[i].gen_ini + ) + + # start-up variables + JuMP.@variable(subproblem, up[i=1:num_of_generators], Bin) + JuMP.@variable(subproblem, down[i=1:num_of_generators], Bin) + + # demand slack + JuMP.@variable(subproblem, demand_slack >= 0.0) + JuMP.@variable(subproblem, neg_demand_slack >= 0.0) + + # cost variables + JuMP.@variable(subproblem, startup_costs[i=1:num_of_generators] >= 0.0) + JuMP.@variable(subproblem, shutdown_costs[i=1:num_of_generators] >= 0.0) + JuMP.@variable(subproblem, fuel_costs[i=1:num_of_generators] >= 0.0) + JuMP.@variable(subproblem, om_costs[i=1:num_of_generators] >= 0.0) + + # generation bounds + JuMP.@constraint(subproblem, genmin[i=1:num_of_generators], gen[i].out >= commit[i].out * generators[i].pmin) + JuMP.@constraint(subproblem, genmax[i=1:num_of_generators], gen[i].out <= commit[i].out * generators[i].pmax) + + # ramping + # we do not need a case distinction as we defined initial_values + JuMP.@constraint(subproblem, rampup[i=1:num_of_generators], gen[i].out - gen[i].in <= generators[i].ramp_up * commit[i].in + generators[i].pmin * (1-commit[i].in)) + JuMP.@constraint(subproblem, rampdown[i=1:num_of_generators], gen[i].in - gen[i].out <= generators[i].ramp_dw * commit[i].out + generators[i].pmin * (1-commit[i].out)) + + # start-up and shut-down + # we do not need a case distinction as we defined initial_values + JuMP.@constraint(subproblem, startup[i=1:num_of_generators], up[i] >= commit[i].out - commit[i].in) + JuMP.@constraint(subproblem, shutdown[i=1:num_of_generators], down[i] >= commit[i].in - commit[i].out) + + # additional storage state + JuMP.@variable( + subproblem, + 0.0 <= storage_level[j = 1:num_of_storages] <= storages[j].level_max, + SDDP.State, + initial_value = storages[j].level_ini, + epsilon=binaryPrecision + ) + + # additional storage generation + JuMP.@variable( + subproblem, + 0.0 <= storage_gen[j = 1:num_of_storages] <= storages[j].gen_max, + ) + + # additional storage pumping + JuMP.@variable( + subproblem, + 0.0 <= storage_pump[j = 1:num_of_storages] <= storages[j].pump_max, + ) + + # additional storage level balance + JuMP.@constraint( + subproblem, + level_balance[j=1:num_of_storages], storage_level[j].out == storage_level[j].in + storage_pump[j] * storages[j].pump_eff - storage_gen[j] / storages[j].gen_eff + inflow[j,stage] + ) + + # additional storage end level + if stage == num_of_stages + JuMP.@constraint(subproblem, storage_end[j=1:num_of_storages], storage_level[j].out >= storages[j].level_end) + end + + # load balance + JuMP.@constraint(subproblem, load, sum(gen[i].out for i in 1:num_of_generators) + demand_slack - neg_demand_slack + sum(storage_gen[j] - storage_pump[j] for j in 1:num_of_storages) == demand[stage] ) + + # costs + JuMP.@constraint(subproblem, startupcost[i=1:num_of_generators], num_of_stages/24 * generators[i].su_cost * up[i] == startup_costs[i]) + JuMP.@constraint(subproblem, shutdowncost[i=1:num_of_generators], generators[i].sd_cost * down[i] == shutdown_costs[i]) + JuMP.@constraint(subproblem, fuelcost[i=1:num_of_generators], generators[i].fuel_cost * gen[i].out == fuel_costs[i]) + JuMP.@constraint(subproblem, omcost[i=1:num_of_generators], generators[i].om_cost * gen[i].out == om_costs[i]) + + # define stage objective + su_costs = subproblem[:startup_costs] + sd_costs = subproblem[:shutdown_costs] + f_costs = subproblem[:fuel_costs] + om_costs = subproblem[:om_costs] + demand_slack = subproblem[:demand_slack] + neg_demand_slack = subproblem[:neg_demand_slack] + SDDP.@stageobjective(subproblem, + sum(su_costs[i] + sd_costs[i] + f_costs[i] + om_costs[i] for i in 1:num_of_generators) + + demand_slack * demand_penalty + neg_demand_slack * demand_penalty) + end + + end + + return model +end + +model_config()