3535from switch_model .tools .graph .cli_graph import main as graph_main
3636from switch_model .utilities .patches import patch_pyomo
3737from switch_model .utilities .results_info import save_info , add_info , ResultsInfoSection
38+ import switch_model .utilities .gurobi_aug # We keep this line here to ensure that 'gurobi_aug' gets registered as a solver
3839
3940
4041def main (
@@ -155,9 +156,6 @@ def debug(type, value, tb):
155156 # create an instance (also reports time spent reading data and loading into model)
156157 instance = model .load_inputs (attach_data_portal = attach_data_portal )
157158
158- if instance .options .warm_start :
159- warm_start (instance )
160-
161159 #### Below here, we refer to instance instead of model ####
162160
163161 instance .pre_solve ()
@@ -190,6 +188,13 @@ def debug(type, value, tb):
190188 # We no longer need model (only using instance) so we can garbage collect it to optimize our memory usage
191189 del model
192190
191+ if instance .options .warm_start_mip :
192+ if instance .options .verbose :
193+ timer .step_time ()
194+ warm_start_mip (instance )
195+ if instance .options .verbose :
196+ print (f"Loaded warm start inputs in { timer .step_time_as_str ()} ." )
197+
193198 if instance .options .reload_prior_solution :
194199 print ("Loading prior solution..." )
195200 reload_prior_solution_from_pickle (instance , instance .options .outputs_dir )
@@ -276,14 +281,14 @@ def debug(type, value, tb):
276281 )
277282
278283
279- def warm_start (instance ):
284+ def warm_start_mip (instance ):
280285 """
281- This function loads in the variables from a previous run
282- and starts out our model at these variables to make it reach
283- a solution faster.
286+ This function loads the results from a previous run into the Pyomo variables.
287+ This allows Gurobi's Mixed Integer Programming algorithm to "warm start" (start closer to the solution).
288+ Warm starting only works in Gurobi if the initial values don't violate any constraints
289+ (i.e. valid but not optimal solution).
284290 """
285- warm_start_timer = StepTimer ()
286- warm_start_dir = os .path .join (instance .options .warm_start , "outputs" )
291+ warm_start_dir = os .path .join (instance .options .warm_start_mip , "outputs" )
287292 if not os .path .isdir (warm_start_dir ):
288293 warnings .warn (
289294 f"Path { warm_start_dir } does not exist and cannot be used to warm start solver. Warm start skipped."
@@ -310,8 +315,6 @@ def warm_start(instance):
310315 # If the index isn't valid that's ok, just don't warm start that variable
311316 pass
312317
313- print (f"Loaded warm start inputs in { warm_start_timer .step_time_as_str ()} ." )
314-
315318
316319def reload_prior_solution_from_pickle (instance , outdir ):
317320 with open (os .path .join (outdir , "results.pickle" ), "rb" ) as fh :
@@ -524,10 +527,16 @@ def define_arguments(argparser):
524527 # whether that does the same thing as --solver-options-string so we don't reuse the same name.
525528 argparser .add_argument (
526529 "--solver-options-string" ,
527- default = None ,
530+ default = "" ,
528531 help = "A quoted string of options to pass to the model solver. Each option must be of the form option=value. "
529532 "(e.g., --solver-options-string \" mipgap=0.001 primalopt='' advance=2 threads=1\" )" ,
530533 )
534+ argparser .add_argument (
535+ "--solver-method" ,
536+ default = None ,
537+ type = int ,
538+ help = "Specify the solver method to use." ,
539+ )
531540 argparser .add_argument (
532541 "--keepfiles" ,
533542 action = "store_true" ,
@@ -639,6 +648,12 @@ def define_arguments(argparser):
639648 action = "store_true" ,
640649 help = "Save the solution to a pickle file after model is solved to allow for later inspection via --reload-prior-solution." ,
641650 )
651+ argparser .add_argument (
652+ "--save-warm-start" ,
653+ default = False ,
654+ action = "store_true" ,
655+ help = "Save warm_start.pickle to the outputs which allows future runs to warm start from this one." ,
656+ )
642657 argparser .add_argument (
643658 "--interact" ,
644659 default = False ,
@@ -685,10 +700,27 @@ def define_arguments(argparser):
685700 help = "Number of threads to be used while solving. Currently only supported for Gurobi" ,
686701 )
687702
703+ argparser .add_argument (
704+ "--warm-start-mip" ,
705+ default = None ,
706+ help = "Enables warm start for a Mixed Integer problem by specifying the "
707+ "path to a previous scenario. Warm starting only works if the solution to the previous solution"
708+ "is also a feasible (but not necessarily optimal) solution to the current scenario." ,
709+ )
710+
688711 argparser .add_argument (
689712 "--warm-start" ,
690713 default = None ,
691- help = "Path to folder of directory to use for warm start" ,
714+ help = "Enables warm start for a LP Problem by specifying the path to the previous scenario. Note"
715+ " that all variables must be the same between the previous and current scenario." ,
716+ )
717+
718+ argparser .add_argument (
719+ "--gurobi-make-mps" ,
720+ default = False ,
721+ action = "store_true" ,
722+ help = "Instead of solving just output a Gurobi .mps file that can be used for debugging numerical properties."
723+ " See https://github.com/staadecker/lp-analyzer/ for details." ,
692724 )
693725
694726
@@ -719,7 +751,7 @@ def add_recommended_args(argparser):
719751 "--recommended-debug" ,
720752 default = False ,
721753 action = "store_true" ,
722- help = 'Equivalent to running with all of the following options: --solver gurobi -v --sorted-output -- keepfiles --tempdir temp --stream-output -- symbolic-solver-labels --log-run --debug --solver-options-string "method=2 BarHomogeneous=1 FeasibilityTol=1e-5"' ,
754+ help = "Same as --recommended but adds the flags -- keepfiles --tempdir temp --symbolic-solver-labels which are useful when debugging Gurobi." ,
723755 )
724756
725757
@@ -748,8 +780,10 @@ def parse_recommended_args(args):
748780 "--log-run" ,
749781 "--debug" ,
750782 "--graph" ,
783+ "--solver-method" ,
784+ "2" , # Method 2 is barrier solve which is much faster than default
751785 ] + args
752- solver_options_string = "BarHomogeneous=1 FeasibilityTol=1e-5 method=2 "
786+ solver_options_string = "BarHomogeneous=1 FeasibilityTol=1e-5"
753787 if options .recommended_fast :
754788 solver_options_string += " crossover=0"
755789 args = ["--solver-options-string" , solver_options_string ] + args
@@ -851,6 +885,18 @@ def solve(model):
851885 solver = model .solver
852886 solver_manager = model .solver_manager
853887 else :
888+ # If we need warm start switch the solver to our augmented version that supports warm starting
889+ if model .options .warm_start is not None or model .options .save_warm_start :
890+ if model .options .solver not in ("gurobi" , "gurobi_direct" ):
891+ raise NotImplementedError (
892+ "Warm start functionality requires --solver gurobi"
893+ )
894+ model .options .solver = "gurobi_aug"
895+
896+ if model .options .warm_start is not None :
897+ # Method 1 (dual simplex) is required since it supports warm starting.
898+ model .options .solver_method = 1
899+
854900 # Create a solver object the first time in. We don't do this until a solve is
855901 # requested, because sometimes a different solve function may be used,
856902 # with its own solver object (e.g., with runph or a parallel solver server).
@@ -860,28 +906,35 @@ def solve(model):
860906 # Note previously solver was saved in model however this is very memory inefficient.
861907 solver = SolverFactory (model .options .solver , solver_io = model .options .solver_io )
862908
863- # If this option is enabled, gurobi will output an IIS to outputs\iis.ilp.
864- if model .options .gurobi_find_iis :
865- # Enable symbolic labels since otherwise we can't debug the .ilp file.
866- model .options .symbolic_solver_labels = True
909+ if model .options .gurobi_find_iis and model .options .gurobi_make_mps :
910+ raise Exception ("Can't use --gurobi-find-iis with --gurobi-make-mps." )
867911
868- # If no string is passed make the string empty so we can add to it
869- if model . options . solver_options_string is None :
870- model .options .solver_options_string = ""
912+ if model . options . gurobi_find_iis or model . options . gurobi_make_mps :
913+ # If we are outputting a file we want to enable symbolic labels to help debugging
914+ model .options .symbolic_solver_labels = True
871915
916+ # If this option is enabled, gurobi will output an IIS to outputs\iis.ilp.
917+ if model .options .gurobi_find_iis :
872918 # Add to the solver options 'ResultFile=iis.ilp'
873919 # https://stackoverflow.com/a/51994135/5864903
874- iis_file_path = os .path .join (model .options .outputs_dir , "iis.ilp" )
875- model .options .solver_options_string += " ResultFile={}" .format (
876- iis_file_path
920+ model .options .solver_options_string += " ResultFile=iis.ilp"
921+ if model .options .gurobi_make_mps :
922+ # Output the input file and set time limit to zero to ensure it doesn't actually solve
923+ model .options .solver_options_string += (
924+ f" ResultFile=problem.mps TimeLimit=0"
877925 )
878926
879927 if model .options .threads :
928+ model .options .solver_options_string += f" Threads={ model .options .threads } "
929+
930+ if model .options .solver_method is not None :
880931 # If no string is passed make the string empty so we can add to it
881932 if model .options .solver_options_string is None :
882933 model .options .solver_options_string = ""
883934
884- model .options .solver_options_string += f" Threads={ model .options .threads } "
935+ model .options .solver_options_string += (
936+ f" method={ model .options .solver_method } "
937+ )
885938
886939 solver_manager = SolverManagerFactory (model .options .solver_manager )
887940
@@ -896,9 +949,17 @@ def solve(model):
896949 else None ,
897950 )
898951
899- if model .options .warm_start is not None :
952+ if model .options .warm_start_mip is not None or model . options . warm_start is not None :
900953 solver_args ["warmstart" ] = True
901954
955+ if model .options .warm_start is not None :
956+ solver_args ["read_warm_start" ] = os .path .join (
957+ model .options .warm_start , "outputs" , "warm_start.pickle"
958+ )
959+
960+ if model .options .save_warm_start :
961+ solver_args ["write_warm_start" ] = os .path .join ("outputs" , "warm_start.pickle" )
962+
902963 # drop all the unspecified options
903964 solver_args = {k : v for (k , v ) in solver_args .items () if v is not None }
904965
0 commit comments