source: trunk/coopr/pysp/ph.py @ 1757

Last change on this file since 1757 was 1757, checked in by jwatson, 11 years ago

Added preliminary checkpointing of PH state via Python's pickle'ing mechanism. Simultaneously slick and scary. This commit only deals with the writing of the state - restoration is in a subsequent commit.

File size: 58.2 KB
Line 
1#  _________________________________________________________________________
2#
3#  Coopr: A COmmon Optimization Python Repository
4#  Copyright (c) 2008 Sandia Corporation.
5#  This software is distributed under the BSD License.
6#  Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
7#  the U.S. Government retains certain rights in this software.
8#  For more information, see the Coopr README.txt file.
9#  _________________________________________________________________________
10
11import sys
12import pyutilib
13import types
14from coopr.pyomo import *
15import copy
16import os.path
17import traceback
18import copy
19from coopr.opt import SolverResults,SolverStatus
20from coopr.opt.base import SolverFactory
21from coopr.opt.parallel import SolverManagerFactory
22import time
23import types
24import pickle
25
26from scenariotree import *
27from phutils import *
28
29from pyutilib.plugin.core import ExtensionPoint
30
31from coopr.pysp.phextension import IPHExtension
32
33class ProgressiveHedging(object):
34
35   #
36   # a utility intended for folks who are brave enough to script rho setting in a python file.
37   #
38
39   def setRhoAllScenarios(self, variable_value, rho_expression):
40
41      variable_name = None
42      variable_index = None
43
44      if isVariableNameIndexed(variable_value.name) is True:
45
46         variable_name, variable_index = extractVariableNameAndIndex(variable_value.name)
47
48      else:
49
50         variable_name = variable_value.name
51         variable_index = None
52     
53      new_rho_value = rho_expression()
54
55      if self._verbose is True:
56         print "Setting rho="+str(new_rho_value)+" for variable="+variable_value.name
57
58      for instance_name, instance in self._instances.items():
59
60         rho_param = getattr(instance, "PHRHO_"+variable_name)
61         rho_param[variable_index] = new_rho_value
62
63   #
64   # a simple utility to count the number of continuous and discrete variables in a set of instances.
65   # unused variables are ignored, and counts include all active indices. returns a pair - num-discrete,
66   # num-continuous.
67   #
68
69   def compute_variable_counts(self):
70
71      num_continuous_vars = 0
72      num_discrete_vars = 0
73     
74      for stage in self._scenario_tree._stages:
75
76         if stage != self._scenario_tree._stages[-1]: # no blending over the final stage
77
78            for tree_node in stage._tree_nodes:
79
80               for (variable, index_template, variable_indices) in stage._variables:
81
82                  variable_name = variable.name
83
84                  variable_type = variable.domain
85
86                  for index in variable_indices:
87
88                     is_used = True # until proven otherwise                     
89                     for scenario in tree_node._scenarios:
90                        instance = self._instances[scenario._name]
91                        if getattr(instance,variable_name)[index].status == VarStatus.unused:
92                           is_used = False
93
94                     if is_used is True:                       
95
96                        # JPW TBD: ideally, we want an "is-discrete" check in the logic below, which COOPR doesn't currently support.
97                        if isinstance(variable_type, IntegerSet) or isinstance(variable_type, BooleanSet):
98                           num_discrete_vars = num_discrete_vars + 1
99                        else:
100                           num_continuous_vars = num_continuous_vars + 1
101
102      return (num_discrete_vars, num_continuous_vars)
103
104   #
105   # checkpoint the current PH state via pickle'ing. the input iteration count
106   # simply serves as a tag to create the output file name. everything with the
107   # exception of the _ph_plugin, _solver_manager, and _solver attributes are
108   # pickled. currently, plugins fail in the pickle process, which is fine as
109   # JPW doesn't think you want to pickle plugins (particularly the solver and
110   # solver manager) anyway. For example, you might want to change those later,
111   # after restoration - and the PH state is independent of how scenario
112   # sub-problems are solved.
113   #
114
115   def checkpoint(self, iteration_count):
116
117      checkpoint_filename = "checkpoint."+str(iteration_count)
118
119      tmp_ph_plugin = self._ph_plugin
120      tmp_solver_manager = self._solver_manager
121      tmp_solver = self._solver
122
123      self._ph_plugin = None
124      self._solver_manager = None
125      self._solver = None
126     
127      checkpoint_file = open(checkpoint_filename, "w")
128      pickle.dump(self,checkpoint_file)
129      checkpoint_file.close()
130
131      self._ph_plugin = tmp_ph_plugin
132      self._solver_manager = tmp_solver_manager
133      self._solver = tmp_solver
134
135      print "Checkpoint written to file="+checkpoint_filename
136   
137   #
138   # ditto above, but count the number of fixed discrete and continuous variables.
139   # important: once a variable (value) is fixed, it is flagged as unused in the
140   # course of presolve - because it is no longer referenced. this makes sense,
141   # of course; it's just something to watch for. this is an obvious assumption
142   # that we won't be fixing unused variables, which should not be an issue.
143   #
144
145   def compute_fixed_variable_counts(self):
146
147      num_fixed_continuous_vars = 0
148      num_fixed_discrete_vars = 0
149     
150      for stage in self._scenario_tree._stages:
151         
152         if stage != self._scenario_tree._stages[-1]: # no blending over the final stage
153           
154            for tree_node in stage._tree_nodes:
155
156               for (variable, index_template, variable_indices) in stage._variables:
157
158                  variable_name = variable.name
159
160                  variable_type = variable.domain
161
162                  for index in variable_indices:
163
164                     # implicit assumption is that if a variable value is fixed in one
165                     # scenario, it is fixed in all scenarios.
166
167                     is_fixed = False # until proven otherwise
168                     for scenario in tree_node._scenarios:
169                        instance = self._instances[scenario._name]
170                        var_value = getattr(instance,variable_name)[index]
171                        if var_value.fixed is True:
172                           is_fixed = True
173
174                     if is_fixed is True:
175
176                        # JPW TBD: ideally, we want an "is-discrete" check in the logic below, which COOPR doesn't currently support.
177                        if isinstance(variable_type, IntegerSet) or isinstance(variable_type, BooleanSet):
178                           num_fixed_discrete_vars = num_fixed_discrete_vars + 1
179                        else:
180                           num_fixed_continuous_vars = num_fixed_continuous_vars + 1                           
181
182      return (num_fixed_discrete_vars, num_fixed_continuous_vars)
183
184   """ Constructor
185       Arguments:
186          max_iterations        the maximum number of iterations to run PH (>= 0). defaults to 0.
187          rho                   the global rho value (> 0). defaults to 0.
188          rho_setter            an optional name of a python file used to set particular variable rho values.
189          solver                the solver type that PH uses to solve scenario sub-problems. defaults to "cplex".
190          solver_manager        the solver manager type that coordinates scenario sub-problem solves. defaults to "serial".
191          keep_solver_files     do I keep intermediate solver files around (for debugging)? defaults to False.
192          output_solver_log     do I dump the solver log (as it is being generated) to the screen? defaults to False.
193          output_solver_results do I output (for debugging) the detailed solver results, including solutions, for scenario solves? defaults to False.
194          verbose               does the PH object stream debug/status output? defaults to False.
195          output_times          do I output timing statistics? defaults to False (e.g., useful in the case where you want to regression test against baseline output).
196          checkpoint_interval   how many iterations between writing a checkpoint file containing the entire PH state? defaults to 0, indicating never.
197
198   """
199   def __init__(self, *args, **kwds):
200
201      # PH configuration parameters
202      self._rho = 0.0 # a default, global values for rhos.
203      self._rho_setter = None # filename for the modeler to set rho on a per-variable or per-scenario basis.
204      self._max_iterations = 0
205
206      # PH reporting parameters
207      self._verbose = False # do I flood the screen with status output?
208      self._output_continuous_variable_stats = True # when in verbose mode, do I output weights/averages for continuous variables?
209      self._output_solver_results = False
210      self._output_times = False
211
212      # PH run-time variables
213      self._current_iteration = 0 # the 'k'
214      self._xbar = {} # current per-variable averages. maps (node_id, variable_name) -> value
215      self._initialized = False # am I ready to call "solve"? Set to True by the initialize() method.
216
217      # PH solver information / objects.
218      self._solver_type = "cplex"
219      self._solver_manager_type = "serial" # serial or pyro are the options currently available
220     
221      self._solver = None # will eventually be unnecessary once Bill eliminates the need for a solver in the solver manager constructor.
222      self._solver_manager = None 
223     
224      self._keep_solver_files = False
225      self._output_solver_log = False
226
227      # PH convergence computer/updater.
228      self._converger = None
229
230      # PH history
231      self._solutions = {}
232
233      # the checkpoint interval - expensive operation, but worth it for big models.
234      # 0 indicates don't checkpoint.
235      self._checkpoint_interval = 0
236
237      # all information related to the scenario tree (implicit and explicit).
238      self._model = None # not instantiated
239      self._model_instance = None # instantiated
240
241      self._scenario_tree = None
242     
243      self._scenario_data_directory = "" # this the prefix for all scenario data
244      self._instances = {} # maps scenario name to the corresponding model instance
245
246      # for various reasons (mainly hacks at this point), it's good to know whether we're minimizing or maximizing.
247      self._is_minimizing = None
248
249      # global handle to ph extension plugin
250      self._ph_plugin = ExtensionPoint(IPHExtension)
251
252      # PH timing statistics - relative to last invocation.
253      self._init_start_time = None # for initialization() method
254      self._init_end_time = None
255      self._solve_start_time = None # for solve() method
256      self._solve_end_time = None
257      self._cumulative_solve_time = None # seconds, over course of solve()
258      self._cumulative_xbar_time = None # seconds, over course of update_xbars()
259      self._cumulative_weight_time = None # seconds, over course of update_weights()
260
261      # do I disable warm-start for scenario sub-problem solves during PH iterations >= 1?
262      self._disable_warmstarts = False
263
264      # do I retain quadratic objective terms associated with binary variables? in general,
265      # there is no good reason to not linearize, but just in case, I introduced the option.
266      self._retain_quadratic_binary_terms = False
267
268      # PH default tolerances - for use in fixing and testing equality across scenarios,
269      # and other stuff.
270      self._integer_tolerance = 0.00001
271
272      # process the keyword options
273      for key in kwds.keys():
274         if key == "max_iterations":
275            self._max_iterations = kwds[key]
276         elif key == "rho":
277            self._rho = kwds[key]
278         elif key == "rho_setter":
279            self._rho_setter = kwds[key]           
280         elif key == "solver":
281            self._solver_type = kwds[key]
282         elif key == "solver_manager":
283            self._solver_manager_type = kwds[key]           
284         elif key == "keep_solver_files":
285            self._keep_solver_files = kwds[key]
286         elif key == "output_solver_results":
287            self._output_solver_results = kwds[key]
288         elif key == "output_solver_log":
289            self._output_solver_log = kwds[key]
290         elif key == "verbose":
291            self._verbose = kwds[key]
292         elif key == "output_times":
293            self._output_times = kwds[key]
294         elif key == "disable_warmstarts":
295            self._disable_warmstarts = kwds[key]
296         elif key == "retain_quadratic_binary_terms":
297            self._retain_quadratic_binary_terms = kwds[key]
298         elif key == "checkpoint_interval":
299            self._checkpoint_interval = kwds[key]           
300         else:
301            print "Unknown option=" + key + " specified in call to PH constructor"
302
303      # validate all "atomic" options (those that can be validated independently)
304      if self._max_iterations < 0:
305         raise ValueError, "Maximum number of PH iterations must be non-negative; value specified=" + `self._max_iterations`
306      if self._rho <= 0.0:
307         raise ValueError, "Value of rho paramter in PH must be non-zero positive; value specified=" + `self._rho`
308
309      # validate rho setter file if specified.
310      if self._rho_setter is not None:
311         if os.path.exists(self._rho_setter) is False:
312            raise ValueError, "The rho setter script file="+self._rho_setter+" does not exist"
313
314      # validate the checkpoint interval.
315      if self._checkpoint_interval < 0:
316         raise ValueError, "A negative checkpoint interval with value="+str(self._checkpoint_interval)+" was specified in call to PH constructor"
317
318      # construct the sub-problem solver.
319      self._solver = SolverFactory(self._solver_type)
320      if self._solver == None:
321         raise ValueError, "Unknown solver type=" + self._solver_type + " specified in call to PH constructor"
322      if self._keep_solver_files is True:
323         # TBD - this is a bit kludgy, as it requires PH to know/assume that
324         #       it is dealing with a system call solver. the solver factory should take keyword options as a fix.
325         self._solver.keepFiles = True
326
327      # construct the solver manager.
328      self._solver_manager = SolverManagerFactory(self._solver_manager_type)
329      if self._solver_manager is None:
330         raise ValueError, "Failed to create solver manager of type="+self._solver_manager_type+" specified in call to PH constructor"
331
332      # a set of all valid PH iteration indicies is generally useful for plug-ins, so create it here.
333      self._iteration_index_set = Set(name="PHIterations")
334      for i in range(0,self._max_iterations + 1):
335         self._iteration_index_set.add(i)     
336
337      # spit out parameterization if verbosity is enabled
338      if self._verbose is True:
339         print "PH solver configuration: "
340         print "   Max iterations=" + `self._max_iterations`
341         print "   Default global rho=" + `self._rho`
342         if self._rho_setter is not None:
343            print "   Rho initialization file=" + self._rho_setter
344         print "   Sub-problem solver type=" + `self._solver_type`
345         print "   Solver manager type=" + `self._solver_manager_type`
346         print "   Keep solver files? " + str(self._keep_solver_files)
347         print "   Output solver results? " + str(self._output_solver_results)
348         print "   Output solver log? " + str(self._output_solver_log)
349         print "   Output times? " + str(self._output_times)
350         print "   Checkpoint interval="+str(self._checkpoint_interval)
351
352   """ Initialize PH with model and scenario data, in preparation for solve().
353       Constructs and reads instances.
354   """
355   def initialize(self, scenario_data_directory_name=".", model=None, model_instance=None, scenario_tree=None, converger=None):
356
357      self._init_start_time = time.time()
358
359      if self._verbose is True:
360         print "Initializing PH"
361         print "   Scenario data directory=" + scenario_data_directory_name
362
363      if not os.path.exists(scenario_data_directory_name):
364         raise ValueError, "Scenario data directory=" + scenario_data_directory_name + " either does not exist or cannot be read"
365
366      self._scenario_data_directory_name = scenario_data_directory_name
367
368      # IMPT: The input model should be an *instance*, as it is very useful (critical!) to know
369      #       the dimensions of sets, be able to store suffixes on variable values, etc.
370      # TBD: There may be a way to see if a model is initialized - throw an exception if it is not!
371      if model is None:
372         raise ValueError, "A model must be supplied to the PH initialize() method"
373
374      if scenario_tree is None:
375         raise ValueError, "A scenario tree must be supplied to the PH initialize() method"
376
377      if converger is None:
378         raise ValueError, "A convergence computer must be supplied to the PH initialize() method"
379
380      self._model = model
381      self._model_instance = model_instance
382      self._scenario_tree = scenario_tree
383      self._converger = converger
384
385      model_objective = model._component[Objective]
386      self._is_minimizing = (model_objective[ model_objective.keys()[0] ].sense == minimize)
387
388      self._converger.reset()
389
390      # construct instances for each scenario
391      if self._verbose is True:
392         if self._scenario_tree._scenario_based_data == 1:
393            print "Scenario-based instance initialization enabled"
394         else:
395            print "Node-based instance initialization enabled"
396         
397      for scenario in self._scenario_tree._scenarios:
398
399         scenario_instance = None
400
401         if self._verbose is True:
402            print "Creating instance for scenario=" + scenario._name
403
404         try:
405            if self._scenario_tree._scenario_based_data == 1:
406               scenario_data_filename = self._scenario_data_directory_name + os.sep + scenario._name + ".dat"
407               if self._verbose is True:
408                  print "Data for scenario=" + scenario._name + " loads from file=" + scenario_data_filename
409               scenario_instance = (self._model).create(scenario_data_filename)
410            else:
411               scenario_instance = self._model.clone()
412               scenario_data = ModelData()
413               current_node = scenario._leaf_node
414               while current_node is not None:
415                  node_data_filename = self._scenario_data_directory_name + os.sep + current_node._name + ".dat"
416                  if self._verbose is True:
417                     print "Node data for scenario=" + scenario._name + " partially loading from file=" + node_data_filename
418                  scenario_data.add_data_file(node_data_filename)
419                  current_node = current_node._parent
420               scenario_data.read(model=scenario_instance)
421               scenario_instance._load_model_data(scenario_data)
422               scenario_instance.presolve()
423         except:
424            print "Encountered exception in model instance creation - traceback:"
425            traceback.print_exc()
426            raise RuntimeError, "Failed to create model instance for scenario=" + scenario._name
427
428         self._instances[scenario._name] = scenario_instance
429         self._instances[scenario._name].name = scenario._name
430
431      # TBD - Technically, we don't need to add these parameters to the models until after the iteration 0 solve (move later).
432
433      # create PH weight and xbar vectors, on a per-scenario basis, for each variable that is not in the
434      # final stage, i.e., for all variables that are being blended by PH. the parameters are created
435      # in the space of each scenario instance, so that they can be directly and automatically
436      # incorporated into the (appropriately modified) objective function.
437
438      if self._verbose is True:
439         print "Creating weight, average, and rho parameter vectors for scenario instances"
440
441      for (instance_name, instance) in self._instances.items():
442
443         # first, gather all unique variables referenced in any stage
444         # other than the last, independent of specific indices. this
445         # "gather" step is currently required because we're being lazy
446         # in terms of index management in the scenario tree - which
447         # should really be done in terms of sets of indices.
448         # NOTE: technically, the "instance variables" aren't really references
449         # to the variable in the instance - instead, the reference model. this
450         # isn't an issue now, but it could easily become one (esp. in avoiding deep copies).
451         instance_variables = {}
452         
453         for stage in self._scenario_tree._stages:
454           
455            if stage != self._scenario_tree._stages[-1]:
456               
457               for (reference_variable, index_template, reference_indices) in stage._variables:
458                 
459                  if reference_variable.name not in instance_variables.keys():
460                     
461                     instance_variables[reference_variable.name] = reference_variable
462
463         # for each blended variable, create a corresponding ph weight and average parameter in the instance.
464         # this is a bit wasteful, in terms of indices that might appear in the last stage, but that is minor
465         # in the grand scheme of things.
466
467         for (variable_name, reference_variable) in instance_variables.items():
468
469            new_w_index = reference_variable._index # TBD - need to be careful with the shallow copy here
470            new_w_parameter_name = "PHWEIGHT_"+reference_variable.name
471            new_w_parameter = Param(new_w_index,name=new_w_parameter_name)
472            setattr(instance,new_w_parameter_name,new_w_parameter)
473
474            # if you don't explicitly assign values to each index, the entry isn't created - instead, when you reference
475            # the parameter that hasn't been explicitly assigned, you just get the default value as a constant. I'm not
476            # sure if this has to do with the model output, or the function of the model, but I'm doing this to avoid the
477            # issue in any case for now.
478            for index in new_w_index:
479               new_w_parameter[index] = 0.0
480
481            new_avg_index = reference_variable._index # TBD - need to be careful with the shallow copy here
482            new_avg_parameter_name = "PHAVG_"+reference_variable.name
483            new_avg_parameter = Param(new_avg_index,name=new_avg_parameter_name)
484            setattr(instance,new_avg_parameter_name,new_avg_parameter)
485
486            for index in new_avg_index:
487               new_avg_parameter[index] = 0.0
488
489            new_rho_index = reference_variable._index # TBD - need to be careful with the shallow copy here
490            new_rho_parameter_name = "PHRHO_"+reference_variable.name
491            new_rho_parameter = Param(new_rho_index,name=new_rho_parameter_name)
492            setattr(instance,new_rho_parameter_name,new_rho_parameter)
493
494            for index in new_rho_index:
495               new_rho_parameter[index] = self._rho
496
497      # if specified, run the user script to initialize variable rhos at their whim.
498      if self._rho_setter is not None:
499
500         print "Executing user rho set script from filename=", self._rho_setter
501         execfile(self._rho_setter)
502
503      # create parameters to store variable statistics (of general utility) at each node in the scenario tree.
504
505      if self._verbose is True:
506         print "Creating variable statistic (min/avg/max) parameter vectors for scenario tree nodes"
507
508      for stage in self._scenario_tree._stages:
509         if stage != self._scenario_tree._stages[-1]:
510
511            # first, gather all unique variables referenced in this stage
512            # this "gather" step is currently required because we're being lazy
513            # in terms of index management in the scenario tree - which
514            # should really be done in terms of sets of indices.
515            stage_variables = {}
516            for (reference_variable, index_template, reference_index) in stage._variables:
517               if reference_variable.name not in stage_variables.keys():
518                  stage_variables[reference_variable.name] = reference_variable
519
520            # next, create min/avg/max parameters for each variable in the corresponding tree node.
521            # NOTE: the parameter names below could really be empty, as they are never referenced
522            #       explicitly.
523            for (variable_name, reference_variable) in stage_variables.items():
524               for tree_node in stage._tree_nodes:
525
526                  new_min_index = reference_variable._index # TBD - need to be careful with the shallow copy here (and below)
527                  new_min_parameter_name = "NODEMIN_"+reference_variable.name
528                  new_min_parameter = Param(new_min_index,name=new_min_parameter_name)
529                  for index in new_min_index:
530                     new_min_parameter[index] = 0.0
531                  tree_node._minimums[reference_variable.name] = new_min_parameter                                   
532
533                  new_avg_index = reference_variable._index
534                  new_avg_parameter_name = "NODEAVG_"+reference_variable.name
535                  new_avg_parameter = Param(new_avg_index,name=new_avg_parameter_name)
536                  for index in new_avg_index:
537                     new_avg_parameter[index] = 0.0
538                  tree_node._averages[reference_variable.name] = new_avg_parameter
539
540                  new_max_index = reference_variable._index
541                  new_max_parameter_name = "NODEMAX_"+reference_variable.name
542                  new_max_parameter = Param(new_max_index,name=new_max_parameter_name)
543                  for index in new_max_index:
544                     new_max_parameter[index] = 0.0
545                  tree_node._maximums[reference_variable.name] = new_max_parameter                                                     
546
547
548      # indicate that we're ready to run.
549      self._initialized = True
550
551      # cache the number of discrete and continuous variables in the master instance. this value
552      # is of general use, e.g., in the converger classes and in plugins.
553      (self._total_discrete_vars,self._total_continuous_vars) = self.compute_variable_counts()
554      if self._verbose is True:
555         print "Total number of discrete instance variables="+str(self._total_discrete_vars)
556         print "Total number of continuous instance variables="+str(self._total_continuous_vars)
557
558      # track the total number of fixed variables of each category at the end of each PH iteration.
559      (self._total_fixed_discrete_vars,self._total_fixed_continuous_vars) = self.compute_fixed_variable_counts()     
560
561      if self._verbose is True:
562         print "PH successfully created model instances for all scenarios"
563
564      self._init_end_time = time.time()
565
566      if self._verbose is True:
567         print "PH is successfully initialized"
568         if self._output_times is True:
569            print "Initialization time=%8.2f seconds" % (self._init_end_time - self._init_start_time)
570
571      # let plugins know if they care.
572      if len(self._ph_plugin) == 1:
573         self._ph_plugin.service().post_ph_initialization(self)
574
575
576   """ Perform the non-weighted scenario solves and form the initial w and xbars.
577   """   
578   def iteration_0_solve(self):
579
580      if self._verbose is True:
581         print "------------------------------------------------"
582         print "Starting PH iteration 0 solves"
583
584      self._current_iteration = 0
585
586      solve_start_time = time.time()
587
588      # STEP 1: queue up the solves for all scenario sub-problems.
589      #         grab all the action handles for the subsequent barrier sync.
590
591      action_handles = []
592      action_handle_instance_map = {}
593
594      for scenario in self._scenario_tree._scenarios:
595         
596         instance = self._instances[scenario._name]
597         
598         if self._verbose is True:
599            print "Queuing solve for scenario=" + scenario._name
600           
601         # TBD - need to have the solver be able to solve a particular instance, with a specific objective
602
603         # there's nothing to warm-start from in iteration 0, so don't include the keyword.
604         # the reason you don't want to include it is that some solvers don't know how to handle
605         # the keyword at all (despite it being false). you might want to solve iteration 0 solves
606         # using some other solver.
607
608         new_action_handle = self._solver_manager.queue(instance, opt=self._solver, tee=self._output_solver_log)
609         action_handle_instance_map[scenario._name] = new_action_handle
610
611         action_handles.append(new_action_handle)
612
613      # STEP 2: barrier sync for all scenario sub-problem solves.
614      if self._verbose is True:
615         print "Waiting for scenario sub-problem solves"
616      self._solver_manager.wait_all(action_handles)
617      if self._verbose is True:     
618         print "Scenario sub-problem solves completed"     
619
620      solve_end_time = time.time()
621      self._cumulative_solve_time += (solve_end_time - solve_start_time)
622
623      if self._output_times is True:
624         print "Aggregate sub-problem solve time=%8.2f" % (solve_end_time - solve_start_time)
625
626      # STEP 3: Load the results!
627      for scenario_name, action_handle in action_handle_instance_map.items():
628
629         if self._verbose is True:         
630            print "Processing results for scenario=", scenario_name, " completed successfully"         
631
632         instance = self._instances[scenario_name]
633         results = self._solver_manager.get_results(action_handle)
634
635         if len(results.solution) == 0:
636            results.write(num=1)
637            raise RuntimeError, "Solve failed for scenario="+scenario_name+"; no solutions generated"
638
639         if self._output_solver_results is True:
640            print "Results for scenario=",scenario_name
641            results.write(num=1)           
642
643         instance.load(results)
644
645         if self._verbose is True:                 
646            print "Successfully loaded solution for scenario=",scenario_name
647
648
649      # TBD - save the solutions for history tracking
650
651      if self._verbose is True:
652         print "Successfully completed PH iteration 0 solves - solution statistics:"
653         print "         Scenario              Objective                  Value"
654         for scenario in self._scenario_tree._scenarios:
655            instance = self._instances[scenario._name]
656            for objective_name in instance._component[Objective]:
657               objective = instance._component[Objective][objective_name]
658               # TBD: I don't know how to deal with objective arrays, so I'll assume they aren't there
659               print "%20s       %15s     %14.4f" % (scenario._name, objective.name, objective._data[None].expr())
660         print "------------------------------------------------"
661
662   #
663   # recompute the averages, minimum, and maximum statistics for all variables to be blended by PH, i.e.,
664   # not appearing in the final stage. technically speaking, the min/max aren't required by PH, but they
665   # are used often enough to warrant their computation and it's basically free if you're computing the
666   # average.
667   #
668   def update_variable_statistics(self):
669
670      start_time = time.time()
671     
672      for stage in self._scenario_tree._stages:
673         
674         if stage != self._scenario_tree._stages[-1]: # no blending over the final stage
675           
676            for tree_node in stage._tree_nodes:
677               
678               for (variable, index_template, variable_indices) in stage._variables:
679                 
680                  variable_name = variable.name
681                     
682                  avg_parameter_name = "PHAVG_"+variable_name
683                 
684                  for index in variable_indices:
685                     min = float("inf")
686                     avg = 0.0
687                     max = float("-inf")
688                     node_probability = 0.0
689                     
690                     is_used = True # until proven otherwise                     
691                     for scenario in tree_node._scenarios:
692                       
693                        instance = self._instances[scenario._name]
694                       
695                        if getattr(instance,variable_name)[index].status == VarStatus.unused:
696                           is_used = False
697                        else:                       
698                           node_probability += scenario._probability
699                           var_value = getattr(instance, variable.name)[index].value
700                           if var_value < min:
701                              min = var_value
702                           avg += (scenario._probability * var_value)
703                           if var_value > max:
704                              max = var_value
705
706                     if is_used is True:
707                        tree_node._minimums[variable.name][index] = min
708                        tree_node._averages[variable.name][index] = avg / node_probability
709                        tree_node._maximums[variable.name][index] = max                       
710
711                        # distribute the newly computed average to the xbar variable in
712                        # each instance/scenario associated with this node. only do this
713                        # if the variable is used!
714                        for scenario in tree_node._scenarios:
715                           instance = self._instances[scenario._name]
716                           avg_parameter = getattr(instance, avg_parameter_name)
717                           avg_parameter[index] = avg / node_probability
718
719      end_time = time.time()
720      self._cumulative_xbar_time += (end_time - start_time)
721
722   def update_weights(self):
723
724      # because the weight updates rely on the xbars, and the xbars are node-based,
725      # I'm looping over the tree nodes and pushing weights into the corresponding scenarios.
726      start_time = time.time()     
727
728      for stage in self._scenario_tree._stages:
729         
730         if stage != self._scenario_tree._stages[-1]: # no blending over the final stage, so no weights to worry about.
731           
732            for tree_node in stage._tree_nodes:
733
734               for (variable, index_template, variable_indices) in stage._variables:
735
736                  variable_name = variable.name
737                  weight_parameter_name = "PHWEIGHT_"+variable_name
738                  rho_parameter_name = "PHRHO_"+variable_name
739
740                  for index in variable_indices:
741
742                     tree_node_average = tree_node._averages[variable.name][index]()
743
744                     for scenario in tree_node._scenarios:
745
746                        instance = self._instances[scenario._name]
747
748                        if getattr(instance,variable.name)[index].status != VarStatus.unused:
749
750                           # get the weight and rho parameters for this variable/index combination.
751                           rho_value = getattr(instance, rho_parameter_name)[index]()
752                           current_variable_weight = getattr(instance, weight_parameter_name)[index]()
753                                               
754                           # if I'm maximizing, invert value prior to adding (hack to implement negatives)
755                           if self._is_minimizing is False:
756                              current_variable_weight = (-current_variable_weight)
757                           current_variable_value = getattr(instance,variable.name)[index]()
758                           new_variable_weight = current_variable_weight + rho_value * (current_variable_value - tree_node_average)
759                           # I have the correct updated value, so now invert if maximizing.
760                           if self._is_minimizing is False:
761                              new_variable_weight = (-new_variable_weight)
762                           getattr(instance, weight_parameter_name)[index].value = new_variable_weight
763
764      # we shouldn't have to re-simplify the expression, as we aren't adding any constant-variable terms - just modifying parameters.
765
766      end_time = time.time()
767      self._cumulative_weight_time += (end_time - start_time)
768
769   def form_iteration_k_objectives(self):
770
771      if self._verbose is True:
772         print "Forming PH-weighted objective"
773
774      # for each blended variable (i.e., those not appearing in the final stage),
775      # add the linear and quadratic penalty terms to the objective.
776      for instance_name, instance in self._instances.items():
777         
778         objective_name = instance._component[Objective].keys()[0]         
779         objective = instance._component[Objective][objective_name]         
780         objective_expression = objective._data[None].expr # TBD: we don't deal with indexed expressions (not even sure how to interpret them)
781         # the quadratic expression is really treated as just a list - eventually should be treated as a full expression.
782         quad_expression = 0.0
783         
784         for stage in self._scenario_tree._stages:
785
786            # skip the last stage, as no blending occurs
787            if stage != self._scenario_tree._stages[-1]:
788               # find the instance objective.
789               # TBD - for simiplicity, I'm assuming a single objective - we should select "active" objectives later.
790               #       also, can create a quadratic objective and leave the original alone.
791
792               for (reference_variable, index_template, variable_indices) in stage._variables:
793
794                  variable_name = reference_variable.name
795                  variable_type = reference_variable.domain
796
797                  w_parameter_name = "PHWEIGHT_"+variable_name
798                  w_parameter = instance._component[param._ParamBase][w_parameter_name]
799                 
800                  average_parameter_name = "PHAVG_"+variable_name
801                  average_parameter = instance._component[param._ParamBase][average_parameter_name]
802
803                  rho_parameter_name = "PHRHO_"+variable_name
804                  rho_parameter = instance._component[param._ParamBase][rho_parameter_name]
805
806                  instance_variable = instance._component[var._VarBase][variable_name]
807
808                  for index in variable_indices:
809
810                     if (instance_variable[index].status is not VarStatus.unused) and (instance_variable[index].fixed is False):
811
812                        # TBD - if maximizing, here is where you would want "-=" - however, if you do this, the collect/simplify process chokes.
813                        objective_expression += (w_parameter[index] * instance_variable[index])
814
815                        if isinstance(variable_type, BooleanSet) is True:
816
817                           if self._retain_quadratic_binary_terms is False:
818                              # this rather ugly form of the linearized quadratic expression term is required
819                              # due to a pyomo bug - the form (rho/2) * (x+y+z) chokes in presolve when distributing
820                              # over the sum.
821                              new_term = (rho_parameter[index] / 2.0 * instance_variable[index]) - \
822                                         (rho_parameter[index] * average_parameter[index] * instance_variable[index]) + \
823                                         (rho_parameter[index] / 2.0 * average_parameter[index] * average_parameter[index])                             
824                              if objective.sense is minimize:
825                                 objective_expression += new_term
826                              else:
827                                 objective_expression -= new_term                                 
828                           else:
829                              quad_expression += (rho_parameter[index] * (instance_variable[index] - average_parameter[index]) ** 2)
830
831                        else:
832
833                           quad_expression += (rho_parameter[index] * (instance_variable[index] - average_parameter[index]) ** 2)                           
834                   
835         # strictly speaking, this probably isn't necessary - parameter coefficients won't get
836         # pre-processed out of the expression tree. however, if the under-the-hood should change,
837         # we'll be covered.
838         objective_expression.simplify(instance)
839         instance._component[Objective][objective_name]._data[None].expr = objective_expression
840         # if we are linearizing everything, then nothing will appear in the quadratic expression -
841         # don't add the empty "0.0" expression to the objective.
842         if quad_expression != 0.0:
843           instance._component[Objective][objective_name]._quad_subexpr = quad_expression
844
845   def iteration_k_solve(self):
846
847     if self._verbose is True:
848        print "------------------------------------------------"       
849        print "Starting PH iteration " + str(self._current_iteration) + " solves"
850
851     # cache the objective values generated by PH for output at the end of this function.
852     ph_objective_values = {}
853
854     solve_start_time = time.time()
855
856     # STEP 1: queue up the solves for all scenario sub-problems.
857     #         grab all the action handles for the subsequent barrier sync.
858
859     action_handles = []
860     action_handle_instance_map = {}
861
862     for scenario in self._scenario_tree._scenarios:     
863
864        instance = self._instances[scenario._name]
865
866        if self._verbose is True:
867           print "Queuing solve for scenario=" + scenario._name
868
869        # IMPT: You have to re-presolve, as the simple presolver collects the linear terms together. If you
870        # don't do this, you won't see any chance in the output files as you vary the problem parameters!
871        # ditto for instance fixing!
872        instance.presolve()
873
874        # TBD - need to have the solver be able to solve a particular instance, with a specific objective
875
876        # once past iteration 0, there is always a feasible solution from which to warm-start.
877        # however, you might want to disable warm-start when the solver is behaving badly (which does happen).
878        new_action_handle = None
879        if self._disable_warmstarts is False:
880           new_action_handle = self._solver_manager.queue(instance, opt=self._solver, warmstart=True, tee=self._output_solver_log)
881        else:
882           new_action_handle = self._solver_manager.queue(instance, opt=self._solver, tee=self._output_solver_log)           
883
884        action_handle_instance_map[scenario._name] = new_action_handle
885
886        action_handles.append(new_action_handle)
887
888     # STEP 2: barrier sync for all scenario sub-problem solves.
889     if self._verbose is True:           
890        print "Waiting for scenario sub-problem solves"
891     self._solver_manager.wait_all(action_handles)
892     if self._verbose is True:               
893        print "Scenario sub-problem solves completed"
894       
895     solve_end_time = time.time()
896     self._cumulative_solve_time += (solve_end_time - solve_start_time)
897
898     if self._output_times is True:
899        print "Aggregate sub-problem solve time=%8.2f" % (solve_end_time - solve_start_time)
900
901     # STEP 3: Load the results!
902     for scenario_name, action_handle in action_handle_instance_map.items():
903
904        instance = self._instances[scenario_name]
905        results = self._solver_manager.get_results(action_handle)
906
907        if len(results.solution) == 0:
908           raise RuntimeError, "Solve failed for scenario="+scenario_name+"; no solutions generated"
909
910        if self._output_solver_results is True:
911           print "Results for scenario=", scenario_name
912           results.write(num=1)           
913
914        instance.load(results)
915
916        if self._verbose is True:                 
917           print "Successfully loaded solution for scenario=", scenario_name
918
919        # we're assuming there is a single solution.
920        # the "value" attribute is a pre-defined feature of any solution - it is relative to whatever
921        # objective was selected during optimization, which of course should be the PH objective.
922        ph_objective_values[instance.name] = float(results.solution(0).value)
923
924     if self._verbose is True:
925        print "Successfully completed PH iteration " + str(self._current_iteration) + " solves - solution statistics:"
926        print "  Scenario             PH Objective             Cost Objective"
927        for scenario in self._scenario_tree._scenarios:
928           instance = self._instances[scenario._name]
929           for objective_name in instance._component[Objective]:
930              objective = instance._component[Objective][objective_name]
931              # TBD: I don't know how to deal with objective arrays, so I'll assume they aren't there
932              print "%20s       %18.4f     %14.4f" % (scenario._name, ph_objective_values[scenario._name], 0.0)
933
934   def solve(self):
935
936      self._solve_start_time = time.time()
937      self._cumulative_solve_time = 0.0
938      self._cumulative_xbar_time = 0.0
939      self._cumulative_weight_time = 0.0
940
941      print "Starting PH"
942
943      if self._initialized == False:
944         raise RuntimeError, "PH is not initialized - cannot invoke solve() method"
945
946      print "Initiating PH iteration=" + `self._current_iteration`
947
948      self.iteration_0_solve()
949
950      # update variable statistics prior to any output.
951      self.update_variable_statistics()     
952
953      if self._verbose is True:
954         print "Variable values following scenario solves:"
955         self.pprint(False,False,True,False)
956
957      # let plugins know if they care.
958      if len(self._ph_plugin) == 1:
959         self._ph_plugin.service().post_iteration_0_solves(self)
960
961      # update the fixed variable statistics.
962      (self._total_fixed_discrete_vars,self._total_fixed_continuous_vars) = self.compute_fixed_variable_counts()               
963
964      if self._verbose is True:
965         print "Number of discrete variables fixed=",self._total_fixed_discrete_vars," (total=",self._total_discrete_vars,")"
966         print "Number of continuous variables fixed=",self._total_fixed_continuous_vars," (total=",self._total_continuous_vars,")"
967
968      self._converger.update(self._current_iteration, self, self._scenario_tree, self._instances)
969      print "Convergence metric=%12.4f" % self._converger.lastMetric()
970
971      self.update_weights()
972
973      if self._max_iterations > 0:
974         self.form_iteration_k_objectives()
975
976      # let plugins know if they care.
977      if len(self._ph_plugin) == 1:
978         self._ph_plugin.service().post_iteration_0(self)
979
980      # checkpoint if it's time - which it always is after iteration 0,
981      # if the interval is >= 1!
982      if (self._checkpoint_interval > 0):
983         self.checkpoint(0)
984
985      # there is an upper bound on the number of iterations to execute -
986      # the actual bound depends on the converger supplied by the user.
987      for i in range(1, self._max_iterations+1):
988
989         self._current_iteration = self._current_iteration + 1                 
990
991         print "Initiating PH iteration=" + `self._current_iteration`         
992
993         if self._verbose is True:
994            print "Variable averages and weights prior to scenario solves:"
995            self.pprint(True,True,False,False)
996
997         self.iteration_k_solve()
998
999         # update variable statistics prior to any output.
1000         self.update_variable_statistics()
1001         
1002         if self._verbose is True:
1003            print "Variable values following scenario solves:"
1004            self.pprint(False,False,True,False)
1005
1006         # we don't technically have to do this at the last iteration,
1007         # but with checkpointing and re-starts, you're never sure
1008         # when you're executing the last iteration.
1009         self.update_weights()
1010
1011         # let plugins know if they care.
1012         if len(self._ph_plugin) == 1:
1013            self._ph_plugin.service().post_iteration_k_solves(self)
1014
1015         # update the fixed variable statistics.
1016         (self._total_fixed_discrete_vars,self._total_fixed_continuous_vars) = self.compute_fixed_variable_counts()               
1017
1018         if self._verbose is True:
1019            print "Number of discrete variables fixed=",self._total_fixed_discrete_vars," (total=",self._total_discrete_vars,")"
1020            print "Number of continuous variables fixed=",self._total_fixed_continuous_vars," (total=",self._total_continuous_vars,")"
1021
1022         # let plugins know if they care.
1023         if len(self._ph_plugin) == 1:
1024            self._ph_plugin.service().post_iteration_k(self)
1025
1026         # at this point, all the real work of an iteration is complete.
1027
1028         # checkpoint if it's time.
1029         if (self._checkpoint_interval > 0) and (i % self._checkpoint_interval is 0):
1030            self.checkpoint(i)
1031
1032         # check for early termination.
1033         self._converger.update(self._current_iteration, self, self._scenario_tree, self._instances)
1034         print "Convergence metric=%12.4f" % self._converger.lastMetric()
1035
1036         if self._converger.isConverged(self) is True:
1037            if self._total_discrete_vars == 0:
1038               print "PH converged - convergence metric is below threshold="+str(self._converger._convergence_threshold)
1039            else:
1040               print "PH converged - convergence metric is below threshold="+str(self._converger._convergence_threshold)+" or all discrete variables are fixed"               
1041            break
1042
1043         # if we're terminating due to exceeding the maximum iteration count, print a message
1044         # indicating so - otherwise, you get a quiet, information-free output trace.
1045         if i == self._max_iterations:
1046            print "Halting PH - reached maximal iteration count="+str(self._max_iterations)
1047
1048      print "PH complete"
1049
1050      if self._verbose is True:
1051         print "Convergence history:"
1052         self._converger.pprint()
1053
1054      print "Final variable values:"
1055      self.pprint(False,False,True,True)         
1056
1057      print "Final costs:"
1058      self._scenario_tree.pprintCosts(self._instances)
1059
1060      self._solve_end_time = time.time()
1061
1062      if (self._verbose is True) and (self._output_times is True):
1063         print "Overall run-time=   %8.2f seconds" % (self._solve_end_time - self._solve_start_time)
1064
1065      # let plugins know if they care.
1066      if len(self._ph_plugin) == 1:
1067         self._ph_plugin.service().post_ph_execution(self)                                 
1068
1069   #
1070   # prints a summary of all collected time statistics
1071   #
1072   def print_time_stats(self):
1073
1074      print "PH run-time statistics (user):"
1075
1076      print "Initialization time=  %8.2f seconds" % (self._init_end_time - self._init_start_time)
1077      print "Overall solve time=   %8.2f seconds" % (self._solve_end_time - self._solve_start_time)
1078      print "Scenario solve time=  %8.2f seconds" % self._cumulative_solve_time
1079      print "Average update time=  %8.2f seconds" % self._cumulative_xbar_time
1080      print "Weight update time=   %8.2f seconds" % self._cumulative_weight_time
1081
1082   #
1083   # a utility to determine whether to output weight / average / etc. information for
1084   # a variable/node combination. when the printing is moved into a callback/plugin,
1085   # this routine will go there. for now, we don't dive down into the node resolution -
1086   # just the variable/stage.
1087   #
1088   def should_print(self, stage, variable, variable_indices):
1089
1090      if self._output_continuous_variable_stats is False:
1091
1092         variable_type = variable.domain         
1093
1094         if (isinstance(variable_type, IntegerSet) is False) and (isinstance(variable_type, BooleanSet) is False):
1095
1096            return False
1097
1098      return True
1099     
1100   #
1101   # pretty-prints the state of the current variable averages, weights, and values.
1102   # inputs are booleans indicating which components should be output.
1103   #
1104   def pprint(self, output_averages, output_weights, output_values, output_fixed):
1105
1106      # TBD - write a utility routine to figure out the longest identifier width for indicies and other names,
1107      #       to make the output a bit more readable.
1108
1109      if self._initialized is False:
1110         raise RuntimeError, "PH is not initialized - cannot invoke pprint() method"         
1111     
1112      # print tree nodes and associated variable/xbar/ph information in stage-order
1113      for stage in self._scenario_tree._stages:
1114
1115         # we don't blend in the last stage, so we don't current care about printing the associated information.
1116         if stage != self._scenario_tree._stages[-1]:
1117
1118            print "\tStage=" + stage._name
1119
1120            num_outputs_this_stage = 0 # tracks the number of outputs on a per-index basis.
1121
1122            for (variable, index_template, variable_indices) in stage._variables:
1123
1124               variable_name = variable.name
1125
1126               if self.should_print(stage, variable, variable_indices) is True:
1127
1128                  num_outputs_this_variable = 0 # track, so we don't output the variable names unless there is an entry to report.
1129
1130                  for index in variable_indices:               
1131
1132                     weight_parameter_name = "PHWEIGHT_"+variable_name
1133
1134                     num_outputs_this_index = 0 # track, so we don't output the variable index more than once.
1135
1136                     for tree_node in stage._tree_nodes:                 
1137
1138                        # determine if the variable/index pair is used across the set of scenarios (technically,
1139                        # it should be good enough to check one scenario). ditto for "fixed" status. fixed does
1140                        # imply unused (see note below), but we care about the fixed status when outputting
1141                        # final solutions.
1142
1143                        is_used = True # should be consistent across scenarios, so one "unused" flags as invalid.
1144                        is_fixed = False
1145
1146                        for scenario in tree_node._scenarios:
1147                           instance = self._instances[scenario._name]
1148                           variable_value = getattr(instance,variable_name)[index]
1149                           if variable_value.status == VarStatus.unused:
1150                              is_used = False
1151                           if variable_value.fixed is True:
1152                              is_fixed = True
1153 
1154                        # IMPT: this is far from obvious, but variables that are fixed will - because
1155                        #       presolve will identify them as constants and eliminate them from all
1156                        #       expressions - be flagged as "unused" and therefore not output.
1157
1158                        if ((output_fixed is True) and (is_fixed is True)) or (is_used is True):
1159
1160                              minimum_value = tree_node._minimums[variable_name][index]()
1161                              maximum_value = tree_node._maximums[variable_name][index]()
1162
1163                              num_outputs_this_stage = num_outputs_this_stage + 1                           
1164                              num_outputs_this_variable = num_outputs_this_variable + 1
1165                              num_outputs_this_index = num_outputs_this_index + 1
1166
1167                              if num_outputs_this_variable == 1:
1168                                 print "\t\tVariable=",variable_name
1169
1170                              if num_outputs_this_index == 1:
1171                                 print "\t\t\tIndex:", indexToString(index)                             
1172
1173                              print "\t\t\t\tTree Node=",tree_node._name,"\t\t (Scenarios: ",                             
1174                              for scenario in tree_node._scenarios:
1175                                 print scenario._name," ",
1176                                 if scenario == tree_node._scenarios[-1]:
1177                                    print ")"
1178                           
1179                              if output_values is True:
1180                                 average_value = tree_node._averages[variable_name][index]()
1181                                 print "\t\t\t\tValues: ",                       
1182                                 for scenario in tree_node._scenarios:
1183                                    instance = self._instances[scenario._name]
1184                                    this_value = getattr(instance,variable_name)[index].value
1185                                    print "%12.4f" % this_value,
1186                                    if scenario == tree_node._scenarios[-1]:
1187                                       print "    Max-Min=%12.4f" % (maximum_value-minimum_value),
1188                                       print "    Avg=%12.4f" % (average_value),
1189                                       print ""
1190                              if output_weights:
1191                                 print "\t\t\t\tWeights: ",
1192                                 for scenario in tree_node._scenarios:
1193                                    instance = self._instances[scenario._name]
1194                                    print "%12.4f" % getattr(instance,weight_parameter_name)[index].value,
1195                                    if scenario == tree_node._scenarios[-1]:
1196                                       print ""
1197                              if output_averages:
1198                                 print "\t\t\t\tAverage: %12.4f" % (tree_node._averages[variable_name][index].value)
1199
1200            if num_outputs_this_stage == 0:
1201               print "\t\tNo non-converged variables in stage"
1202
1203            # cost variables aren't blended, so go through the gory computation of min/max/avg.
1204            # TBD: possibly move these into the tree node anyway, as they are useful in general.
1205            # TBD: these should also be probability-weighted - fix with above-mentioned TBD fix.
1206            # we currently always print these.
1207            cost_variable_name = stage._cost_variable[0].name
1208            cost_variable_index = stage._cost_variable[1]
1209            if cost_variable_index is None:
1210               print "\t\tCost Variable=" + cost_variable_name
1211            else:
1212               print "\t\tCost Variable=" + cost_variable_name + indexToString(cost_variable_index)           
1213            for tree_node in stage._tree_nodes:
1214               print "\t\t\tTree Node=" + tree_node._name + "\t\t (Scenarios: ",
1215               for scenario in tree_node._scenarios:
1216                  print scenario._name," ",
1217                  if scenario == tree_node._scenarios[-1]:
1218                     print ")"
1219               maximum_value = 0.0
1220               minimum_value = 0.0
1221               sum_values = 0.0
1222               num_values = 0
1223               first_time = True
1224               print "\t\t\tValues: ",                       
1225               for scenario in tree_node._scenarios:
1226                   instance = self._instances[scenario._name]
1227                   this_value = getattr(instance,cost_variable_name)[cost_variable_index].value
1228                   print "%12.4f" % this_value,
1229                   num_values += 1
1230                   sum_values += this_value
1231                   if first_time is True:
1232                      first_time = False
1233                      maximum_value = this_value
1234                      minimum_value = this_value
1235                   else:
1236                      if this_value > maximum_value:
1237                         maximum_value = this_value
1238                      if this_value < minimum_value:
1239                         minimum_value = this_value
1240                   if scenario == tree_node._scenarios[-1]:
1241                      print "    Max-Min=%12.4f" % (maximum_value-minimum_value),
1242                      print "    Avg=%12.4f" % (sum_values/num_values),
1243                      print ""
1244           
Note: See TracBrowser for help on using the repository browser.