Module coinor.grumpy.BBTree

Expand source code
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import hex
from builtins import range
from past.utils import old_div
# BB.py
#
#  Copyright 2009, 2010 Google Inc.
#  Copyright 2007 University of Pittsburgh.
#  Copyright 2012, 2013 Lehigh University
#  Google coding done by Brady Hunsaker.
#  U of Pittsburgh coding done by Osman Ozaltin and Brady Hunsaker.
#  Lehigh University coding done by Ted Ralphs and Aykut Bulut
#
#  This file was part of BAK (Branch-and-bound Analysis Kit).
#  It has now been incporporated into GrUMPy (Graphics for Understanding
#  Mathematical Programming in Python).
#
#  The contents of this file are subject to the Eclipse Public License
#  1.0.  (the "License"); you may not use this file except in
#  compliance with the License. You should have received a copy of
#  the Common Public License along with STOP.
#
#  Software distributed under the License is distributed on an "AS
#  IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
#  implied. See the License for the specific language governing
#  rights and limitations under the License.
#
# For developers: Please keep the code style consistent with the Python
# style guide for Google's Summer of Code, except use 4 spaces to indent:
#   http://code.google.com/p/soc/wiki/PythonStyleGuide

__author__ = 'Brady Hunsaker, Osman Ozaltin, Ted Ralphs, Aykut Bulut'
__maintainer__ = 'Aykut Bulut (aykut@lehigh.edu)'

"""
This package is for visualizing branch-and-bound. It also contains
a basic branch-and-bound implementation primarily for classroom and educational
use.

Communication with solvers is through a grammar described in separate
documentation. Solvers can interface to this class in a number of
different ways and a number of different types of images may be created.

Images at intervals that can be specified on the command line as well as after
new incumbent solutions are found.

Note that the generation of tree images takes significantly longer than other
images because every node appears in the image.
"""

import math
import random
import re
import subprocess
import sys, os
import tempfile
import optparse
try:
    from src.gimpy import BinaryTree, XDOT_INSTALLED, MATPLOTLIB_INSTALLED, DOT2TEX_INSTALLED
    from src.gimpy import PIL_INSTALLED, ETREE_INSTALLED
    from src.gimpy import quote_if_necessary as quote
except ImportError:
    from coinor.gimpy import BinaryTree, XDOT_INSTALLED, MATPLOTLIB_INSTALLED, DOT2TEX_INSTALLED
    from coinor.gimpy import PIL_INSTALLED, ETREE_INSTALLED
    from coinor.gimpy import quote_if_necessary as quote
from io import StringIO
from pulp import LpVariable, lpSum, LpProblem, LpMaximize, LpConstraint
from pulp import LpStatus, value
from .forecasting import ForecastingChainedSequences

if MATPLOTLIB_INSTALLED:
    import matplotlib.pyplot as plt

if PIL_INSTALLED:
    from PIL import Image as PIL_Image

# branch strategy
BRANCH_STRATEGY = None
# search strategy
SEARCH_STRATEGY = None
# branching strategies
MOST_FRACTIONAL = 'Most Fraction'
FIXED_BRANCHING = 'Fixed Branching'
PSEUDOCOST_BRANCHING = 'Pseudocost Branching'
# search strategies
DEPTH_FIRST = 'Depth First'
BEST_FIRST = 'Best First'
BEST_ESTIMATE = 'Best Estimate'
INFINITY = sys.maxsize

DOT2TEX_TEMPLATE = r'''
\documentclass[landscape]{article}
\usepackage[x11names, rgb]{xcolor}
\usepackage[<<textencoding>>]{inputenc}
\usepackage{tikz}
\usetikzlibrary{snakes,arrows,shapes}
\usepackage{amsmath}
\usepackage[margin=2cm,nohead]{geometry}%
<<startpreprocsection>>%
\usepackage[active,auctex]{preview}
<<endpreprocsection>>%
<<gvcols>>%
<<cropcode>>%
<<docpreamble>>%

\begin{document}
\pagestyle{empty}
%
<<startpreprocsection>>%
<<preproccode>>
<<endpreprocsection>>%
%
<<startoutputsection>>
\enlargethispage{100cm}
% Start of code
% \begin{tikzpicture}[anchor=mid,>=latex',join=bevel,<<graphstyle>>]
\resizebox{\textwidth}{!}{
\begin{tikzpicture}[>=latex',join=bevel,<<graphstyle>>]
\pgfsetlinewidth{1bp}
<<figpreamble>>%
<<drawcommands>>
<<figpostamble>>%
\end{tikzpicture}
% End of code
}
<<endoutputsection>>
%
\end{document}
%
<<startfigonlysection>>
\begin{tikzpicture}[>=latex,join=bevel,<<graphstyle>>]
\pgfsetlinewidth{1bp}
<<figpreamble>>%
<<drawcommands>>
<<figpostamble>>%
\end{tikzpicture}
<<endfigonlysection>>
'''

class BBTree(BinaryTree):
    """
    Methods to process and visualize information about a b&b tree. It can
    process an output file (in a specific format, see BAK project in COIN-OR) of
    a solver that has three information. See run.py in examples directory fot
    this use. Moreover it implements a branch and bound method that can solve
    binary programs (0-1 variables only) using PuLP as an LP solver. It provides
    different branching and searching strategies. See test_strategies.py in test
    directory.

    This is the main class of GrUMPy. It inherits BinaryTree from GIMPy and
    keeps the entire branch-and-bound tree in self.
    """
    def __init__(self, **attrs):
        if not 'layout' in attrs:
            attrs['layout']= 'bak'
        BinaryTree.__init__(self, **attrs)
        # User-controlled constant values
        self._label = ''
        self._filename = None
        self._logscaley = False
        self._fathom = False
        self._wait_for_keypress = True
        self._edge_limit = 1000000
        # current time, updated each time we read a new line
        self._time = 0.0
        # use at most NUM nodes of each type in tree images; zero means no limit
        self._sample_tree = 0
        # Instance-dependent constant values
        self._optimization_sense = None
        self._start_time = None
        self._histogram_lower_bound = None
        self._histogram_upper_bound = None
        self._scatterplot_lower_bound = None
        self._scatterplot_upper_bound = None
        self._integer_infeasibility_lower_bound = None
        self._integer_infeasibility_upper_bound = None
        # Changing reference values
        self._image_counter = 0
        self._next_image_time = None
        self._incumbent_value = None
        self._incumbent_parent = None
        self._new_integer_solution = False
        self._max_objective_value = None
        self._min_objective_value = None
        self._max_integer_infeasibility_sum = None
        # List of incumbent path data files, for the all incumbent paths image
        self._incumbent_path_datafiles = []
        # Objects for measuring and predicting progress
        self._objective_gap_forecaster = ForecastingChainedSequences()
        self._sum_subtree_gaps_forecaster = ForecastingChainedSequences()
        self._sum_subtree_gaps_scale = 1.0
        self._previous_incumbent_value = None  # Only needed for SSG
        if 'display' in attrs:
            self.set_display_mode(attrs['display'])
        else:
            self.set_display_mode('off')

    def process_file(self, file_name):
        self._filename = file_name
        input_file = open(file_name, 'r')
        # Parse all the lines
        for line in input_file:
            self.ProcessLine(line)
            if self.root is not None:
                self.display()
        input_file.close()

    def write_as_dynamic_gexf(self, filename, mode = "Dot"):
        if not GEXF_INSTALLED:
            print('Gexf not installed. Exiting.')
            return
        if mode == 'Dot':
            try:
                gexf = Gexf("Mike O'Sullivan", "Dynamic graph file")
                graph = gexf.addGraph("directed", "dynamic", "Dynamic graph")
                objAtt = graph.addNodeAttribute("obj", "0.0", "float")
                currAtt = graph.addNodeAttribute("current", "1.0",
                                                 "integer", "dynamic")
                node_names = self.get_node_list()
                for name in node_names:
                    node = self.get_node(name)
                    if node.get("step") is None:
                        raise Exception("Node without step in BBTree",
                                        "node =", node)
                    curr_step = '%s' % node.get("step")
                    next_step = "%s" % (node.get("step") + 1)
                    n = graph.addNode(name, node.get_label(), start=curr_step)
                    if node.get("obj") is None:
                        raise Exception("Node without objective in BBTree",
                                        "node =", node)
                    n.addAttribute(objAtt, "%s" % node.get("obj"))
                    n.addAttribute(currAtt, "1", start=curr_step, end=next_step)
                    n.addAttribute(currAtt, "0", start=next_step)
                edge_names = self.get_edge_list()
                for i, (m_name, n_name) in enumerate(edge_names):
                    edge = self.get_edge(m_name, n_name)
                    if edge.get("step") is None:
                        raise Exception("Edge without step in BBTree",
                                        "edge =", (m_name, n_name))
                    curr_step = "%s" % edge.get("step")
                    graph.addEdge(i, m_name, n_name, start=curr_step)
                output_file = open(filename + ".gexf", "w")
                gexf.write(output_file)
            except Exception as e:
                print(e)
                print("No .gexf file created")
        else:
            raise Exception("Only Dot mode supported in write_as_dynamic_gexf")

    def set_display_mode(self, mode):
        if mode is 'off':
            self.attr['display'] = mode
        elif mode is 'matplotlib':
            if MATPLOTLIB_INSTALLED:
                self.attr['display'] = 'matplotlib'
            else:
                print('Matplotlib is not installed. Display is set to off.')
                self.attr['display'] = 'off'
        elif mode is 'PIL':
            if PIL_INSTALLED:
                self.attr['display'] = 'PIL'
            else:
                print('PIL is not installed. Display is set to off.')
                self.attr['display'] = 'off'
        elif mode is 'xdot':
            if XDOT_INSTALLED:
                self.attr['display'] = 'xdot'
            else:
                print('Xdot is not installed. Display is set to off.')
                self.attr['display'] = 'off'
        elif mode is 'file':
            self.attr['display'] = 'file'
        elif mode is 'matplotlib':
            self.attr['display'] = 'matplotlib'
        else:
            raise Exception('%s is not a valid display mode.' %mode)

    def display(self, item = 'all', basename = 'graph', format='png',
                count=None, pause=False, wait_for_click=True):
        '''
        Displays/Saves images requested. BranchAndBound method calls this method
        to visualize the branch and bound tree.
        '''
        if self.attr['layout'] != 'bak':
            if 'init_log_cond' in self.root.attr:
                max_log_cond = 0
                for n in list(self.nodes.values()):
                    if 'init_log_cond' in n.attr:
                        max_log_cond = max(n.attr['init_log_cond'], max_log_cond)
                for n in list(self.nodes.values()):
                    if 'init_log_cond' in n.attr:
                        log_begin = n.attr['init_log_cond']
                        log_end = n.attr['final_log_cond']
                        normalized_cond = (1-old_div(log_begin,max_log_cond))
                        color = str(hex(int(normalized_cond*256))[2:]) if normalized_cond >= .0625 else '0' + str(hex(int(normalized_cond*256))[2:])
                        n.attr['label'] = '%.0f \n %.0f' % (log_begin, log_end)
                        n.attr['color'] = '#' + color*3
                        n.attr['fillcolor'] = '#' + color*3
                        n.attr['style'] = 'filled'
                    else:
                        n.attr['label'] = ' '
            BinaryTree.display(self, pause = pause, wait_for_click = wait_for_click)
            return
        if self.attr['display'] is 'off':
            return
        if self.attr['display'] is 'matplotlib':
            gnuplot_script = None
            if item=='all':
                self.display_all()
            elif item=='tree':
                gnuplot_script = self.GenerateTreeImage()
            elif item=='scatterplot':
                gnuplot_script = self.GenerateScatterplot()
            elif item=='histogram':
                gnuplot_script = self.GenerateHistogram()
            elif item=='incumbent':
                gnuplot_script = self.GenerateIncumbentPath()
            elif item=='forecast':
                gnuplot_script = self.GenerateForecastImages()
            else:
                raise Exception('Unknown display() method argument %s' %item)
            if gnuplot_script is not None:
                self.display_image(gnuplot_script)
            # clean auxilary files.
            histogram_files = [f for f in os.listdir(".")
                               if f.startswith("histogram")]
            incumbent_files = [f for f in os.listdir(".")
                               if f.startswith("incumbentpath")]
            scatterplot_files = [f for f in os.listdir(".")
                                 if f.startswith("scatterplot")]
            t_fathomed_files = [f for f in os.listdir(".")
                                if f.startswith("tree_fathomed")]
            t_infeasible_files = [f for f in os.listdir(".")
                                if f.startswith("tree_infeasible")]
            t_pregnant_files = [f for f in os.listdir(".")
                                if f.startswith("tree_pregnant")]
            t_integer_files = [f for f in os.listdir(".")
                               if f.startswith("tree_integer")]
            t_branched_files = [f for f in os.listdir(".")
                               if f.startswith("tree_branched")]
            bak_filelist = (histogram_files + incumbent_files +
                            scatterplot_files + t_fathomed_files +
                            t_integer_files + t_branched_files +
                            t_infeasible_files + t_pregnant_files)
            for f in bak_filelist:
                os.remove(f)
        elif self.attr['display'] is 'xdot':
            if XDOT_INSTALLED:
                window = xdot.DotWindow()
                window.set_dotcode(self.to_string())
                window.connect('destroy', gtk.main_quit)
                gtk.main()
            else:
                print('Error: xdot not installed. Display disabled.')
                self.attr['display'] = 'off'
        elif self.attr['display'] is 'file':
            if count is not None:
                basename = basename + '_' + str(count)
            if self.attr['layout'] is 'dot2tex':
                if DOT2TEX_INSTALLED:
                    if format != 'pdf' or format != 'ps':
                        print("Dot2tex only supports pdf and ps formats,"+\
                            "falling back to pdf")
                        format = 'pdf'
                    self.set_layout('dot')
                    tex = dot2tex.dot2tex(self.to_string(), autosize=True,
                                          texmode = 'math',
                                          template = DOT2TEX_TEMPLATE)
                    f = open(basename+'.tex', 'w')
                    f.write(tex)
                    f.close()
                    subprocess.call(['latex', basename])
                    if format == 'ps':
                        subprocess.call(['dvips', basename])
                    elif format == 'pdf':
                        subprocess.call(['pdflatex', basename])
                    self.set_layout('dot2tex')
                    # clean auxilary files.
                    aux_filelist = [basename+'.tex', basename+'.log',
                                    basename+'.dvi', basename+'.aux']
                    for f in aux_filelist:
                        os.remove(f)
                else:
                    print("Dot2tex not installed, falling back to graphviz")
                    self.set_layout('dot')
                    self.write(basename+'.'+format, self.get_layout(), format)
            else:
                gnuplot_script = None
                if item=='all':
                    self.display_all()
                elif item=='tree':
                    gnuplot_script = self.GenerateTreeImage()
                elif item=='scatterplot':
                    gnuplot_script = self.GenerateScatterplot()
                elif item=='histogram':
                    gnuplot_script = self.GenerateHistogram()
                elif item=='incumbent':
                    gnuplot_script = self.GenerateIncumbentPath()
                elif item=='forecast':
                    gnuplot_script = self.GenerateForecastImages()
                else:
                    raise Exception('Unknown display() method argument %s' %item)
                if gnuplot_script is not None:
                    self.write_image(gnuplot_script, basename+'.'+format)
                # clean auxilary files.
                histogram_files = [f for f in os.listdir(".")
                                   if f.startswith("histogram")]
                incumbent_files = [f for f in os.listdir(".")
                                   if f.startswith("incumbentpath")]
                scatterplot_files = [f for f in os.listdir(".")
                                     if f.startswith("scatterplot")]
                t_fathomed_files = [f for f in os.listdir(".")
                                    if f.startswith("tree_fathomed")]
                t_infeasible_files = [f for f in os.listdir(".")
                                      if f.startswith("tree_infeasible")]
                t_pregnant_files = [f for f in os.listdir(".")
                                    if f.startswith("tree_pregnant")]
                t_integer_files = [f for f in os.listdir(".")
                                   if f.startswith("tree_integer")]
                t_branched_files = [f for f in os.listdir(".")
                                    if f.startswith("tree_branched")]
                bak_filelist = (histogram_files + incumbent_files +
                                scatterplot_files + t_fathomed_files +
                                t_integer_files + t_branched_files +
                                t_infeasible_files + t_pregnant_files)
                for f in bak_filelist:
                    os.remove(f)
        else:
            raise Exception('Unknown display mode %s' %self.attr['display'])

    def display_all(self):
        '''
        Assumes all the images have the same size.
        '''
        print ('This function is deprected and no longer functions')
        return

        # Old source, just in case
        # if not (MATPLOTLIB_INSTALLED and PIL_INSTALLED:
        #     print('Matplotlib or Pillow not installed. Display disabled')
        #     return
        # tree = self.GenerateTreeImage()
        # scatterplot = self.GenerateScatterplot()
        # histogram = self.GenerateHistogram()
        # incumbent = self.GenerateIncumbentPath()
        # if tree is not None:
        #     imTree = StringIO(tree)
        #     pTree = pygame.image.load(imTree, 'png')
        #     sTree = pTree.get_size()
        #     rTree = pygame.Rect(0,0,sTree[0],sTree[1])
        # if scatterplot is not None:
        #     imScatterplot = StringIO(scatterplot)
        #     pScatterplot = pygame.image.load(imScatterplot, 'png')
        #     sScatterplot = pScatterplot.get_size()
        #     rScatterplot = pygame.Rect(sTree[0],0,sScatterplot[0],
        #                                sScatterplot[1])
        # if histogram is not None:
        #     imHistogram = StringIO(histogram)
        #     pHistogram = pygame.image.load(imHistogram, 'png')
        #     sHistogram = pHistogram.get_size()
        #     rHistogram = pygame.Rect(0,sTree[1],sHistogram[0],sHistogram[1])
        # if incumbent is not None:
        #     imIncumbent = StringIO(incumbent)
        #     pIncumbent = pygame.image.load(imIncumbent, 'png')
        #     sIncumbent = pIncumbent.get_size()
        #     rIncumbent = pygame.Rect(sTree[0],sTree[1],sIncumbent[0],
        #                              sIncumbent[1])
        # screen = pygame.display.set_mode((sTree[0]+sTree[0], sTree[1]+sTree[1]))
        # if tree is not None:
        #     screen.blit(pTree, rTree)
        # if scatterplot is not None:
        #     screen.blit(pScatterplot, rScatterplot)
        # if histogram is not None:
        #     screen.blit(pHistogram, rHistogram)
        # if incumbent is not None:
        #     screen.blit(pIncumbent, rIncumbent)
        # pygame.display.flip()
        # if self._wait_for_keypress:
        #     pause = True
        #     print("Press any key to continue to next image (ESCAPE to disable pausing)")
        # else:
        #     pause = False
        # while pause:
        #     e = pygame.event.poll()
        #     if e.type == pygame.KEYDOWN:
        #         keystate = pygame.key.get_pressed()
        #         if keystate[pygame.K_ESCAPE] != 0:
        #             self._wait_for_keypress = False
        #         break
        #     if e.type == pygame.QUIT:
        #         pause = False
        #         pygame.display.quit()
        #         # sys.exit() exits the whole program and I (aykut) guess it is
        #         # not appropriate here.
        #         #sys.exit()

    def write_image(self, gnuplot_script, filename):
        if not (MATPLOTLIB_INSTALLED and PIL_INSTALLED):
            print('Either matplotlib or Pillow is not installed. Display disabled')
            return
        try:
            p = subprocess.run(['gnuplot'], capture_output = True,
                               input = bytearray(gnuplot_script, 'utf8'))
        except OSError:
            print('''Gnuplot executable not found.
Gnuplot must be installed and in your search path.
After installation, ensure that the PATH variable is properly set.''')
            return
        p.check_returncode()

        if p.stderr:
            print(p.stderr)        

        if isinstance(filename, str):
            with open(filename, "w+b") as f:
                f.write(p.stdout)
        else:
            filename.write(p.stdout)

    def display_image(self, gnuplot_script, pause = False, wait_for_click = True):

        if not (PIL_INSTALLED and MATPLOTLIB_INSTALLED):
            print('Warning: Either matplotlib or Pillow is not installed. Cannot display.')
            return
        
        tmp_fd, tmp_name = tempfile.mkstemp()
        tmp_file = os.fdopen(tmp_fd, 'w+b')
        self.write_image(gnuplot_script, tmp_file)
        tmp_file.close()
        im = PIL_Image.open(tmp_name)
        plt.figure(1)
        plt.clf()
        plt.axis('off')
        plt.imshow(im, interpolation='bilinear' #resample=True
                   #extent = (0, 100, 0, 100)
        )
        if wait_for_click == True:
            plt.draw()
            try:
                if plt.waitforbuttonpress(timeout = 10000):
                    plt.close()
                    exit()
            except:
                exit()
        else:
            plt.show(block=pause)
        im.close()

    def set_label(self, label):
        self._label = label

    def set_logscaley(self, boolean):
        self._logscaley = boolean

    def set_fathom(self, boolean):
        self._fathom = boolean

    def set_edge_limit(self, limit):
        self._edge_limit = limit

    def set_sample_tree(self, number):
        self._sample_tree = number

    def AddOrUpdateNode(self, id, parent_id, branch_direction, status, lp_bound,
                        integer_infeasibility_count, integer_infeasibility_sum,
                        condition_begin = None, condition_end = None,
                        **attrs):
        '''
        This method designed to update nodes (in BAK) but we use it for
        updating/adding arcs. This is because of the tree data structure the
        authors adopted in BAK.
        We can divide these attributes such that some will belong to the edge
        parent_id->id and the others belong to the id node. The following shows
        whether the attribute belongs to edge or node.
        branch direction -> edge
        status -> node
        lp_bound -> node
        integer_infeasibility_count -> node
        integer_infeasibility_sum -> node
        parent_id -> node
        '''
        if (condition_begin is not None) and (condition_end is not None):
            #Figure out the color
            attrs['init_log_cond'] = math.log(condition_begin, 10)
            attrs['final_log_cond'] = math.log(condition_end, 10)
        if id in self.neighbors:
            # node already exists, update attributes
            self.set_node_attr(id, 'status', status)
            self.set_node_attr(id, 'lp_bound', lp_bound)
            self.set_node_attr(id, 'integer_infeasibility_count',
                               integer_infeasibility_count)
            self.set_node_attr(id, 'integer_infeasibility_sum',
                               integer_infeasibility_sum)
            if (condition_begin is not None) and (condition_end is not None):
                self.set_node_attr(id, 'init_log_cond',
                                   math.log(condition_begin, 10))
                self.set_node_attr(id, 'final_log_cond',
                                   math.log(condition_end, 10))
        elif self.root is None:
            self.add_root(id, status = status, lp_bound = lp_bound,
                          integer_infeasibility_count = integer_infeasibility_count,
                          integer_infeasibility_sum = integer_infeasibility_sum,
                          subtree_root = None, **attrs)
        elif parent_id is not None:
            if branch_direction == 'L':
                self.add_left_child(id, parent_id, status = status,
                    lp_bound = lp_bound,
                    integer_infeasibility_count = integer_infeasibility_count,
                    integer_infeasibility_sum = integer_infeasibility_sum,
                    subtree_root = None, **attrs)
            elif branch_direction == 'R':
                self.add_right_child(id, parent_id, status = status,
                    lp_bound = lp_bound,
                    integer_infeasibility_count = integer_infeasibility_count,
                    integer_infeasibility_sum = integer_infeasibility_sum,
                    subtree_root = None, **attrs)
        else:
            print('this should not happen.')
            raise Exception()
        if lp_bound is not None:
            self.UpdateObjectiveValueLimits(lp_bound)
            # Set optimization sense if not yet set
            if self._optimization_sense is None:
                if lp_bound < self.root.get_attr('lp_bound'):
                    self._optimization_sense = 'max'
                elif lp_bound > self.root.get_attr('lp_bound'):
                    self._optimization_sense = 'min'
            if self._optimization_sense == 'min' and lp_bound < self.root.get_attr('lp_bound'):
                print("Switching guess about objective sense to maximization based on bound change")
                self._optimization_sense = 'max'
            if self._optimization_sense == 'max' and lp_bound > self.root.get_attr('lp_bound'):
                print("Switching guess about objective sense to minimization based on bound change")
                self._optimization_sense = 'max'
        if integer_infeasibility_sum is not None:
            if (self._max_integer_infeasibility_sum is None or
                integer_infeasibility_sum >
                self._max_integer_infeasibility_sum):
                self._max_integer_infeasibility_sum = integer_infeasibility_sum

    def IsBetterThan(self, value1, value2):
        """
        Returns True if value1 is better than value2 as an objective value.
        This depends on the optimization sense of the instance.
        Args:
          value1: Float.
          value2: Float.
        Returns:
          True if value1 is better than value2 as an objective value.
        """
        if self._optimization_sense is None:
            print("Optimization sense is not set, assuming sense is miniminzation")
            self._optimization_sense = 'min'
        if self._optimization_sense == 'min':
            return value1 < value2
        else:
            return value1 > value2

    def IsBetterThanIncumbent(self, value):
        """
        Returns True if the passed value is better than current incumbent.
        Args:
          value: Float to use for comparison.
        Returns:
          True if the passed value is better than the current incumbent.
          'Better' is determined by the sense of optimization.
        """
        if self._incumbent_value is None:
            return True
        else:
            return self.IsBetterThan(value, self._incumbent_value)

    def UpdateObjectiveValueLimits(self, value):
        """Updates the min and max objective values if appropriate.
        Args:
          value: Float objective value.
        """
        if self._max_objective_value is None:
            self._max_objective_value = value
            self._min_objective_value = value
        else:
            if value > self._max_objective_value:
                self._max_objective_value = value
            if value < self._min_objective_value:
                self._min_objective_value = value

    def AddProgressMeasures(self):
        # No progress measures if there is no incumbent yet
        if self._incumbent_value is None:
            return
        # Store sum-of-subtree-gaps
        # We need to traverse all nodes unfortunately
        # TODO(bhunsaker): check whether we can just traverse active nodes
        active_node_count = 0
        subtree_bounds = {}
        new_integer_ssg = 0  # Only needed if this is a new integer solution
        for node_id in self.get_node_list():
            status = self.get_node_attr(node_id, 'status')
            if status == 'candidate' or status == 'pregnant':
                lp_bound = self.get_node_attr(node_id, 'lp_bound')
                subtree_root = self.get_node_attr(node_id, 'subtree_root')
                # Optional check for fathomed nodes.
                if (self._fathom and
                    not self.IsBetterThanIncumbent(lp_bound)):
                    continue
                active_node_count += 1
                if (subtree_root not in subtree_bounds or
                    self.IsBetterThan(lp_bound, subtree_bounds[subtree_root])):
                    subtree_bounds[subtree_root] = lp_bound
                if self._new_integer_solution:
                    self.set_node_attr(node_id, 'subtree_root', id)
                    new_integer_ssg += abs(self._incumbent_value - lp_bound)
        # If we have a new integer solution, we need to compute what
        # the measure would be with the previous integer solution for
        # scaling purposes.
        if (self._new_integer_solution and
            self._previous_incumbent_value is not None):
            reference_value = self._previous_incumbent_value
        else:
            reference_value = self._incumbent_value
        sum_subtree_gaps = 0
        for lp_bound in list(subtree_bounds.values()):
            sum_subtree_gaps += abs(reference_value - lp_bound)
        # Start a new sequence if a new integer solution was just found
        if self._new_integer_solution:
            if new_integer_ssg >= 1e-6:
                scale_factor = (old_div(float(sum_subtree_gaps),
                                float(new_integer_ssg)))
            else:
                scale_factor = 1.0
            self._sum_subtree_gaps_forecaster.StartNewSequence(scale_factor)
            # sum_subtree_gaps was based on the previous integer solution;
            # update it now
            sum_subtree_gaps = new_integer_ssg
        self._sum_subtree_gaps_forecaster.AddMeasure(self._time,
                                                     sum_subtree_gaps,
                                                     active_node_count,
                                                     len(self.get_node_list()))
        # Add objective gap measure.  Note that this relies on the
        # active_node_count computed above.
        if self._new_integer_solution:
            self._objective_gap_forecaster.StartNewSequence(1.0)
        if self._optimization_sense == 'min':
            obj_gap = self._incumbent_value - self._min_objective_value
        else:
            obj_gap = self._max_objective_value - self._incumbent_value
        self._objective_gap_forecaster.AddMeasure(self._time, obj_gap,
                                                  active_node_count,
                                                  len(self.get_node_list()))

    def GetImageCounterString(self):
        """
        Returns a string with the image counter.
        """
        return '%03d' % self._image_counter

    def WriteHistogramScript(self, num_bins, bin_width, max_bin_count,
                                 lp_bound, data_filename, output_file):
        """
        Write a Gnuplot script file to generate a histogram image.
        Args:
          num_bins: Integer number of bins for the histogram.
          bin_width: Float width of the bins in terms of objective values.
          max_bin_count: Integer number of the highest bin count.
          lp_bound: Float value of the current LP bound.
          data_filename: String name of the file; used for display purposes.
        """
        # TODO(bhunsaker): add checks for bin_width zero
        if self._incumbent_value is not None:
            incumbent_bin = 1 + ((self._incumbent_value -
                                  self._histogram_lower_bound) // bin_width)
            incumbent_x_coord = 0.5 + (old_div((self._incumbent_value -
                                        self._histogram_lower_bound),
                                       bin_width))
        lp_bound_bin = 1 + ((lp_bound - self._histogram_lower_bound) //
                            bin_width)
        lp_bound_x_coord = 0.5 + (old_div((lp_bound - self._histogram_lower_bound),
                                  bin_width))
        # TODO(bhunsaker): Ask Osman about adjust_xcoord option, which appears
        #    to put the vertical lines at the edge of bins rather than the
        #    true location.
        # Output the Gnuplot script to a file.
        script = ""
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the scatter plot.
        index_string = self.GetImageCounterString()
        output_filename = "histogram."+index_string+".png"
        if output_file:
            script += 'set output "%s"\n' % output_filename
        if self._filename is None:
            script += 'set title "Histogram of LP Bounds"\n'
        else:
            script += ('set title "Histogram of LP Bounds: %s, %s, %.2fs"\n'
                       % (self._filename, self._label, self._time))
        script += 'set xlabel "obj. value"\n'
        script += 'set ylabel "number of nodes"\n'
        if self._logscaley:
            script += 'set logscale y\n'
        script += 'set nokey\n'
        script += 'set tics scale 0.001\n'

        script += 'set xrange [0:%d+1]\n' % num_bins
        if self._logscaley:
            script += 'set yrange [1:%d*1.2]\n' % max_bin_count
        else:
            script += 'set yrange [0:%d*1.2]\n' % max_bin_count
        script += 'set xtics rotate by 90\n'
        # Mark tics for each bin.
        script += 'set xtics ('
        # TODO(bhunsaker): Consider putting this in a loop.
        x_values = ['"%0.2f" %0.2f' %
                    (self._histogram_lower_bound + i * bin_width, i + 0.5)
                    for i in range(num_bins + 1)]
        script += ', '.join(x_values) + ')\n'
        # Plot LP bound and incumbent tics.
        script += 'set x2tics ('
        script += '"%0.2f" %d' % (lp_bound, lp_bound_bin)
        if self._incumbent_value is not None:
            script += ', "%0.2f"%d)\n' % (self._incumbent_value,
                                                  incumbent_bin)
        else:
            script += ')\n'
        plot_parts = []
        # Plot the data points.
        plot_parts.append('\'%s\' with boxes fill solid 0.2' % data_filename)
        # Draw the vertical lp_bound and incumbent lines.
        script += 'set parametric\n'
        script += 'set trange [0:%d*1.5]\n' % max_bin_count
        plot_parts.append('%0.2f,t linetype 2' % lp_bound_x_coord)
        if self._incumbent_value is not None:
            plot_parts.append('%0.2f,t linetype 5' % incumbent_x_coord)
        script += 'plot %s\n' % ', '.join(plot_parts)
        script += 'unset parametric\n'
        script += 'show output\n'
        return script

    def AdjustHistogramEndBins(self, objective_list, num_bins, bin_width,
                               bin_counts, bin_centers, bin_widths):
        """
        Adjusts the two end bins if necessary to make them narrower.
        The two end bins may need to be narrower than the other bins so that
        they do not go past the current incumbent value on one end and the
        current lp bound on the other.  So that the histogram is still correct
        in areas, the height of these bins needs to be adjusted so that the
        area does not change.

        Note that there is likely to be some bias toward taller bins on
        the ends since they always have a point at one end of their width.  It
        may be more accurate visually to ignore or discount that one point when
        determining the bin height, but that is not currently done.

        Args:
          objective_list: List of float objective values.
          num_bins: Integer number of bins.
          bin_width: Float standard width of bins in terms of objective values.
          bin_counts: List of integer counts for each bin.
          bin_centers: List of float coordinates for the center of each bin.
          bin_widths: List of float widths for bins, allowing for individualized
            widths.
        """
        if self._optimization_sense == 'min':
            lp_bound = min(objective_list)
            lower_bound = lp_bound
            if self._incumbent_value is not None:
                upper_bound = self._incumbent_value
            else:
                upper_bound = self._histogram_upper_bound
        else:
            lp_bound = max(objective_list)
            upper_bound = lp_bound
            if self._incumbent_value is not None:
                lower_bound = self._incumbent_value
            else:
                lower_bound = self._histogram_lower_bound
        # The end bins may have unusual centers and widths
        highest_nonempty_bin = int((upper_bound -
                                    self._histogram_lower_bound) // bin_width)
        if (highest_nonempty_bin < num_bins and
            bin_counts[highest_nonempty_bin] > 0):
            highest_x_coord = 0.5 + (old_div((upper_bound -
                                      self._histogram_lower_bound), bin_width))
            highest_nonempty_bin_width, unused_int = math.modf(0.5 +
                                                               highest_x_coord)
            if highest_nonempty_bin_width == 0.0:
                highest_nonempty_bin_width = 1.0
            bin_widths[highest_nonempty_bin] = highest_nonempty_bin_width
            bin_centers[highest_nonempty_bin] = highest_x_coord - (
                old_div(highest_nonempty_bin_width, 2))
            # Scale the height appropriately
            bin_counts[highest_nonempty_bin] /= bin_widths[highest_nonempty_bin]
        lowest_nonempty_bin = int((lower_bound -
                                   self._histogram_lower_bound) // bin_width)
        if bin_counts[lowest_nonempty_bin] > 0:
            lowest_x_coord = 0.5 + (old_div((lower_bound -
                                     self._histogram_lower_bound), bin_width))
            lowest_nonempty_bin_excess, unused_int = math.modf(0.5 +
                                                               lowest_x_coord)
            bin_widths[lowest_nonempty_bin] = 1.0 - lowest_nonempty_bin_excess
            bin_centers[lowest_nonempty_bin] = (
                lowest_x_coord + old_div(bin_widths[lowest_nonempty_bin], 2))
            # Scale the height appropriately
            bin_counts[lowest_nonempty_bin] /= bin_widths[lowest_nonempty_bin]

    def GenerateHistogram(self, output_file = False):
        """
        Generate files necessary for a histogram image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        Args:
          time: Float number of seconds since the start of optimization.
        """
        num_bins = 20
        # Compute the bin width and counts.
        objective_list = []
        for n in self.get_node_list():
            if (self.get_node_attr(n,'status') == 'candidate' or
                self.get_node_attr(n,'status') == 'pregnant'):
                lp_bound = self.get_node_attr(n,'lp_bound')
                if not self.IsBetterThanIncumbent(lp_bound):
                    continue
                objective_list.append(lp_bound)
        # TODO(aykut) added the following check, we need it since we generate
        # histograms real time
        # we can not generate histogram if we do not have upperl and lower
        #bounds
        if len(objective_list)==0 or self._incumbent_value is None:
            return None
        # The first time we create a histogram, set bounds for objective
        # values.
        # TODO(bhunsaker): Consider bounds; talk to Osman.
        if self._histogram_lower_bound is None:
            if self._optimization_sense == 'min':
                self._histogram_lower_bound = min(objective_list)
                if self._incumbent_value is not None:
                    self._histogram_upper_bound = self._incumbent_value
                else:
                    self._histogram_upper_bound = max(objective_list)
            else:
                self._histogram_upper_bound = max(objective_list)
                if self._incumbent_value is not None:
                    self._histogram_lower_bound = self._incumbent_value
                else:
                    self._histogram_lower_bound = min(objective_list)
        bin_width = old_div((self._histogram_upper_bound -
                     self._histogram_lower_bound), float(num_bins))
        bin_counts = [0.0 for i in range(num_bins)]
        for value in objective_list:
            bin = int(math.floor(old_div((value - self._histogram_lower_bound),
                                 bin_width)))
            # Special case for the largest value.
            if (value >= self._histogram_upper_bound and
                value < self._histogram_upper_bound + 1e-6):
                bin = num_bins - 1
            if bin < 0:
                return
            assert bin < num_bins, '%d (%f) !< %d (%f)' % (
                bin, value, num_bins, self._histogram_upper_bound)
            bin_counts[bin] += 1
        max_bin_count = max(bin_counts)
        bin_centers = [i + 1.0 for i in range(len(bin_counts))]
        bin_widths = [1.0 for i in range(len(bin_counts))]
        self.AdjustHistogramEndBins(objective_list, num_bins, bin_width,
                                    bin_counts, bin_centers, bin_widths)
        if self._optimization_sense == 'min':
            lp_bound = min(objective_list)
        else:
            lp_bound = max(objective_list)
        # Output the bin data to a file.
        index_string = self.GetImageCounterString()
        data_filename = 'histogram%s.dat' % index_string
        data_file = open(data_filename, 'w')
        for index in range(len(bin_counts)):
            data_file.write('%f %f %f\n' % (bin_centers[index],
                                            bin_counts[index],
                                            bin_widths[index]))
        data_file.close()
        histogram_script = self.WriteHistogramScript(num_bins, bin_width,
                           max_bin_count, lp_bound, data_filename, output_file)
        # TODO(bhunsaker): Temporary hack
        #   This allows the bounds to be reset until an incumbent is found.
        if self._incumbent_value is None:
            self._histogram_lower_bound = None
            self._histogram_upper_bound = None
        return histogram_script

    def GetImageObjectiveBounds(self, min_value, max_value):
        """
        Return min and max bounds to be used for images.
        Images should use bounds that are slightly wider than observed
        objective values.  Also, the special case of a single value must be
        handled.
        Args:
          min_value: Float minimum objective value.
          max_value: Float maximum objective value.
        Returns:
          A tuple of two float values (lower_bound, upper_bound).
        """
        obj_range = max_value - min_value
        if obj_range > 0:
            image_max_obj = max_value + 0.01 * obj_range
            image_min_obj = min_value - 0.01 * obj_range
        else:
            if max_value >= 0:
                image_max_obj = 1.01 * max_value
            else:
                image_max_obj = 0.99 * max_value
            if min_value >= 0:
                image_min_obj = 0.99 * min_value
            else:
                image_min_obj = 1.01 * min_value
        return (image_min_obj, image_max_obj)

    def WriteScatterplotScript(self, data_filename, output_file):
        """
        Write a Gnuplot script file to generate a scatterplot image.
        Args:
          data_filename: String name of the file; used for display purposes.
        """
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._scatterplot_lower_bound, self._scatterplot_upper_bound)
        index_string = self.GetImageCounterString()
        output_filename = "scatterplot."+index_string+".png"
        script = ""
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the scatter plot.
        if output_file:
            script += 'set output "%s"\n' % output_filename
        if self._filename is None:
            script += 'set title "Scatterplot"\n'
        else:
            script += ('set title "Scatterplot: %s, %s, %ds"\n' % (
                    self._filename, self._label, int(self._time)))
        script += 'set pointsize 0.8\n'
        script += 'set nokey\n'
        script += 'set xlabel \"sum of int. infeas.\"\n'
        script += 'set ylabel \"obj. value\"\n'
        script += ('set xrange [0:%0.6f+2]\n' %
                          self._max_integer_infeasibility_sum)
        script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                          image_max_obj))
        plot_parts = []
        # Plot the data points.
        plot_parts.append('\'%s\' with points pointtype 2 linetype 1' %
                          data_filename)
        # Also plot the incumbent line.
        if self._incumbent_value is not None:
            plot_parts.append('%0.6f linetype 2 linewidth 0.5' %
                              self._incumbent_value)
        # Plot the incumbent's parent if it's available.
        if self._incumbent_parent is not None:
            #incumbent_parent = self.get_node(self._incumbent_parent)
            plot_parts.append('"< echo %0.6f %0.6f" '
                              'with points pointtype 9 pointsize 1.2' %
                              (self.get_node_attr(self._incumbent_parent,
                                                  'integer_infeasibility_sum'),
                               self.get_node_attr(self._incumbent_parent,
                                                  'lp_bound')))
        script += 'plot %s\n' % ', '.join(plot_parts)
        script += 'show output\n'
        return script

    def GenerateScatterplot(self, output_file = False):
        """
        Generate files necessary for a scatterplot image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        Args:
            output_file: if not given the gnuplot image will not be written
        to disk but returned (to be displayed in matplotlib window)
        """
        # Output data points.
        index_string = self.GetImageCounterString()
        data_filename = 'scatterplot%s.dat' % index_string
        data_file = open(data_filename, 'w')
        if self._scatterplot_lower_bound is None:
            bounds = []
        # Write objective values and integer infeasibility sum information
        # for candidate and pregnant nodes.
        for node in self.get_node_list():
            status = self.get_node_attr(node, 'status')
            lp_bound = self.get_node_attr(node,'lp_bound')
            if status == 'candidate' or status == 'pregnant':
                # Optional check for fathomed nodes.
                if (self._fathom and
                    not self.IsBetterThanIncumbent(lp_bound)):
                    continue
                data_file.write('%0.6f %0.6f\n' % (
                        self.get_node_attr(node, 'integer_infeasibility_sum'),
                        lp_bound))
                # Set the image objective bounds the first image.
                if self._scatterplot_lower_bound is None:
                    bounds.append(lp_bound)
        data_file.close()
        if self._scatterplot_lower_bound is None:
            if len(bounds) <= 1:
                return None
            self._scatterplot_lower_bound = min(bounds)
            self._scatterplot_upper_bound = max(bounds)
            # The incumbent overrides a bound if present.
            if self._incumbent_value is not None:
                if self._optimization_sense == 'min':
                    self._scatterplot_upper_bound = self._incumbent_value
                else:
                    self._scatterplot_lower_bound = self._incumbent_value
        scatterplot_script = self.WriteScatterplotScript(data_filename,
                                                         output_file)
        return scatterplot_script

    def WriteIncumbentPathScript(self, data_filename):
        """
        Write a Gnuplot script file to generate an incumbent path image.
        Args:
          data_filename: String name of the file; used for display purposes.
        """
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._scatterplot_lower_bound, self._scatterplot_upper_bound)
        script = ''
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'
        if self._filename is None:
            script += 'set title "Incumbent path"\n'
        else:
            script += ('set title "Incumbent path (%s %.2fs %s)"\n' % (
                    self._filename, self._time, self._label))
        script += 'set pointsize 0.8\n'
        script += 'set nokey\n'
        script += 'set xlabel \"sum of int. infeas.\"\n'
        script += 'set ylabel \"obj. value\"\n'
        script += ('set xrange [0:%0.6f+2]\n' %
                          self._max_integer_infeasibility_sum)
        script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                          image_max_obj))
        # Plot the data points and connecting lines.
        script += ('plot \'%s\' with points pointtype 2, '
                          '\'%s\' with lines linetype 2\n' %
                          (data_filename, data_filename))
        script += 'show output\n'
        return script

    def WriteAllIncumbentPathsScript(self):
        """
        Return a Gnuplot script string to generate an incumbent path image.
        Args:
          data_filenames: List of string names of files.
        """
        data_filenames = self._incumbent_path_datafiles
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._scatterplot_lower_bound, self._scatterplot_upper_bound)
        script = ''
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'

        # Make settings for the scatter plot.
        if self._filename is None:
            script += 'set title "Incumbent paths"\n'
        else:
            script += ('set title "Incumbent paths (%s %.2fs %s)"\n' % (
                    self._filename, self._time, self._label))
        script += 'set pointsize 0.8\n'
        script += 'set nokey\n'
        script += 'set xlabel \"sum of int. infeas.\"\n'
        script += 'set ylabel \"obj. value\"\n'
        script += ('set xrange [0:%0.6f+2]\n' %
                          self._max_integer_infeasibility_sum)
        script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                          image_max_obj))
        # Plot the data points and connecting lines.
        command_list = []
        for filename in data_filenames:
            command_list.append('\'%s\' with points pointtype 2, '
                                '\'%s\' with lines linetype 2' %
                                (filename, filename))
        script += 'plot %s\n' % ','.join(command_list)
        script += 'show output\n'
        return script

    def GenerateIncumbentPath(self):
        """
        Generate files necessary for an incumbent scatterplot path image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        """
        if self._incumbent_parent is None:
            return
        if self._scatterplot_lower_bound is None:
            return
        if self._scatterplot_upper_bound is None:
            return
        index_string = self.GetImageCounterString()
        # Output data points.
        data_filename = 'incumbentpath%s.dat' % index_string
        data_file = open(data_filename, 'w')
        # Write objective values and integer infeasibility sum information
        # for ancestor nodes.
        data_file.write('0 %0.6f\n' % self._incumbent_value)
        parent = self._incumbent_parent
        # TODO(bhunsaker): I think the following assumes a unique value for the
        #   parent of the root.
        while parent != None:
            data_file.write('%0.6f %0.6f\n'
                            % (self.get_node_attr(parent,
                               'integer_infeasibility_sum'),
                             self.get_node_attr(parent, 'lp_bound')))
            parent = self.get_node_attr(parent, 'parent')
        data_file.close()
        self._incumbent_path_datafiles.append(data_filename)
        # Output the Gnuplot script to a file.
        path_script = self.WriteIncumbentPathScript(data_filename)
        return path_script

    def GenerateAllIncumbentPaths(self):
        """
        Generate file for a path image with all incumbent paths.
        Data files were previously generated for each incumbent.  This re-uses
        those files.
        """
        all_path_script = self.WriteAllIncumbentPathsScript()

    def WriteTreeScript(self, additional_lines = None):
        """
        Write a Gnuplot script file to generate a tree image.
        Args:
          additional_lines: String with additional lines to be added to the
            script file.
        """
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._min_objective_value, self._max_objective_value)
        data = ''
        data += 'set terminal png notransparent size 480,360\n'
        data += 'set output "%s"\n' % output_file
        data += 'set nokey\n'
        data += 'set autoscale\n'
        data += 'set tics scale 0.001\n'
        data += 'set pointsize 0.5\n'
        data += 'set xrange [-0.1:1.1]\n'
        data += 'set yrange [%0.6f:%0.6f]\n' % (image_max_obj,
                                                          image_min_obj)
        data += 'set format x ""\n'
        data += 'set ylabel "obj. value"\n'
        if self._filename is None:
            data += 'set title "B&B tree"\n'
        else:
            data += 'set title "B&B tree (%s %.2fs %s)"\n\n' % (
                self._filename, self._time, self._label)
        for line in additional_lines:
            data += line
        return data

    def GetTreeFixedHorizontalPositions(self):
        """
        Returns horizontal positions for all nodes based on fixed positions.
        Returns:
          Dictionary of float horizontal positions, keyed by node id.
        """
        # Statistics needed for horizontal positions.
        horizontal_lower_bound = dict.fromkeys(self.get_node_list(), 0.0)
        horizontal_upper_bound = dict.fromkeys(self.get_node_list(), 1.0)
        horizontal_positions = dict.fromkeys(self.get_node_list())
        horizontal_positions[self.root.name] = 0.5
        # sort node list
        node_id_list = sorted(self.get_node_list())
        node_id_list_int = list(int(n) for n in node_id_list)
        node_id_list_int = sorted(node_id_list_int)
        node_id_list = list(str(n) for n in node_id_list_int)
        for node_id in node_id_list:
            if node_id == self.root.name:
                continue
            parent_id = self.get_node_attr(node_id, 'parent')
            branch_direction = self.get_node_attr(node_id, 'direction')
            if branch_direction == 'R':
                horizontal_lower_bound[node_id] = horizontal_positions[
                    parent_id]
                horizontal_upper_bound[node_id] = horizontal_upper_bound[
                    parent_id]
            elif branch_direction == 'L':
                horizontal_lower_bound[node_id] = horizontal_lower_bound[
                    parent_id]
                horizontal_upper_bound[node_id] = horizontal_positions[
                    parent_id]
            else:
                print('Error: node %s has unsupported branching direction.'\
                    %node_id)
                print('Fixed-position tree images only support L and R ')
                print('branching.')
                sys.exit(1)
            horizontal_positions[node_id] = old_div((
                horizontal_upper_bound[node_id] +
                horizontal_lower_bound[node_id]), 2)
        return horizontal_positions

    def GetTreeHorizontalPositions(self):
        """
        Returns horizontal positions for all nodes.
        Each node is given equal horizontal space.
        Returns:
          Dictionary of float horizontal positions, keyed by node id.
        """
        # Statistics needed for horizontal positions.
        number_descendants = dict.fromkeys(self.get_node_list(), 1)
        # number_descendants includes the key node itself
        horizontal_lower_bound = dict.fromkeys(self.get_node_list(), 0.0)
        horizontal_upper_bound = dict.fromkeys(self.get_node_list(), 1.0)
        horizontal_positions = dict.fromkeys(self.get_node_list())
        visited = dict.fromkeys(self.get_node_list(), False)
        # Count the number of descendants for each node.
        # Do a post-order traversal of the tree.
        node_stack = []
        node_stack.append(self.root.name)
        while node_stack:
            current_node = node_stack[len(node_stack) - 1]
            lchild = self.get_left_child(current_node)
            rchild = self.get_right_child(current_node)
            is_node_added = False
            # Add the next unvisited child to the stack
            if lchild is not None and not visited[quote(lchild)]:
                node_stack.append(lchild)
                is_node_added = True
            if (rchild is not None and not visited[quote(rchild)] and
                is_node_added==False):
                node_stack.append(rchild)
                is_node_added = True
            # If all childs visited, then update number_descendants
            if not is_node_added:
                if lchild is not None:
                    number_descendants[quote(current_node)] += (
                                number_descendants[quote(lchild)])
                if rchild is not None:
                    number_descendants[quote(current_node)] += (
                                number_descendants[quote(rchild)])
                visited[quote(current_node)] = True
                del node_stack[len(node_stack) - 1]
        # Traverse the tree and set horizontal positions.
        # Do a pre-order traversal of the tree.
        node_stack = []
        node_stack.append(self.root.name)
        horizontal_lower_bound[self.root.name] = 0.0
        horizontal_upper_bound[self.root.name] = 1.0
        while node_stack:
            node = node_stack.pop()
            lchild = self.get_left_child(node)
            rchild = self.get_right_child(node)
            direction = None
            number_of_children = 0
            children_list = []
            # Place all children on the stack
            if lchild is not None:
                node_stack.append(lchild)
                number_of_children += 1
                direction = 'L'
                children_list.append(lchild)
            if rchild is not None:
                node_stack.append(rchild)
                number_of_children += 1
                direction = 'R'
                children_list.append(rchild)
            # Convenience variables
            current_lower_bound = horizontal_lower_bound[quote(node)]
            current_range = (horizontal_upper_bound[quote(node)] -
                             horizontal_lower_bound[quote(node)])
            total_descendants = number_descendants[quote(node)]
            sorted_child_labels = sorted(children_list)
            # Determine where to place this node with respect to its children.
            # Put the node in the center, or have more children on the left.
            before_index = int(math.ceil(old_div(number_of_children,2.0)))
            # Exception with a single node that is 'L'
            if number_of_children == 1:
                if direction != 'L':
                    before_index = 0
            cumulative_descendants = 0
            for i, label in enumerate(sorted_child_labels):
                if before_index == i:
                    # Determine the relative position for the current node
                    relative_position = old_div((cumulative_descendants + 0.5), (
                        total_descendants))
                    cumulative_descendants += 1
                # Set bounds for this child
                horizontal_lower_bound[quote(label)] = (
                    current_lower_bound + float(cumulative_descendants) /
                    total_descendants * current_range)
                # Increment cumulative_descendants, which also lets us compute
                # the upper bound.
                cumulative_descendants += number_descendants[quote(label)]
                horizontal_upper_bound[quote(label)] = (
                    current_lower_bound + float(cumulative_descendants) /
                    total_descendants * current_range)
            # Catch the case that the node comes after all its children.
            # This must also work for the case that this is the only node.
            if before_index == len(sorted_child_labels):
                relative_position = old_div((cumulative_descendants + 0.5), (
                    total_descendants))
            # Finally set the position for the current node
            horizontal_positions[quote(node)] = (
                horizontal_lower_bound[quote(node)] + relative_position * (
                    horizontal_upper_bound[quote(node)] -
                    horizontal_lower_bound[quote(node)]))
        return horizontal_positions

    def WriteDataFileFromList(self, filename, data_list):
        """
        Write a list of string data to a file with one entry per line.
        Args:
          filename: String filename to open.
          data_list: List of string values to write.
        """
        outfile = open(filename, 'w')
        for line in data_list:
            outfile.write(line)
        outfile.close()

    def GenerateTreeImage(self, fixed_horizontal_positions = False):
        """
        Generate files necessary for a tree image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        """
        index_string = self.GetImageCounterString()
        if fixed_horizontal_positions:
            name_prefix = 'tree_alt'
            horizontal_positions = self.GetTreeFixedHorizontalPositions()
        else:
            name_prefix = 'tree'
            horizontal_positions = self.GetTreeHorizontalPositions()
        candidate_lines = []
        pregnant_lines = []
        branched_lines = []
        infeasible_lines = []
        fathomed_lines = []
        integer_lines = []
        additional_script_lines = []
        node_list = self.get_node_list()
        print_edges = (len(node_list) <= self._edge_limit)
        for node in node_list:
            node_lp_bound = self.get_node_attr(node, 'lp_bound')
            if self.get_node_attr(node, 'status') == 'candidate':
                # TODO(bhunsaker): add optional fathoming check
                candidate_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'pregnant':
                # TODO(bhunsaker): add optional fathoming check
                pregnant_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'branched':
                branched_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'infeasible':
                infeasible_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'fathomed':
                fathomed_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'integer':
                integer_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            if print_edges and node != self.root.name:
                if True:
                    _parent_id = self.get_node_attr(node, 'parent')
                    additional_script_lines.append(
                     'set arrow from %0.6f, %0.6f to %0.6f, %0.6f nohead lt -1 '
                     'lw 0.2\n' % (horizontal_positions[_parent_id],
                     self.get_node_attr(_parent_id, 'lp_bound'),
                     horizontal_positions[node],
                     self.get_node_attr(node, 'lp_bound')))
        plot_parts = []
        # Plot root node.
        plot_parts.append('"< echo %0.6f %0.6f" w p lt 2 pt 7' %
                          (horizontal_positions[self.root.name],
                           self.root.get_attr('lp_bound')))
        # If desired, sample from the set of nodes rather than plotting all.
        if self._sample_tree:
            sample_size = self._sample_tree
            if len(branched_lines) > sample_size:
                branched_lines = random.sample(branched_lines, sample_size)
            if len(fathomed_lines) > sample_size:
                fathomed_lines = random.sample(fathomed_lines, sample_size)
            if len(infeasible_lines) > sample_size:
                infeasible_lines = random.sample(infeasible_lines, sample_size)
            if len(pregnant_lines) > sample_size:
                pregnant_lines = random.sample(pregnant_lines, sample_size)
            if len(candidate_lines) > sample_size:
                candidate_lines = random.sample(candidate_lines, sample_size)
            if len(integer_lines) > sample_size:
                integer_lines = random.sample(integer_lines, sample_size)
        # Output all data files.  Note that the order below matters.
        if len(branched_lines):
            self.WriteDataFileFromList('%s_branched%s.dat' % (name_prefix,
                                                              index_string),
                                       branched_lines)
            plot_parts.append('\'%s_branched%s.dat\' w p lt rgb "yellow" pt 7' %
                              (name_prefix, index_string))
        if len(fathomed_lines):
            self.WriteDataFileFromList('%s_fathomed%s.dat' % (name_prefix,
                                                              index_string),
                                       fathomed_lines)
            plot_parts.append('\'%s_fathomed%s.dat\' w p lt rgb "light-red" pt 7' %
                              (name_prefix, index_string))
        if len(infeasible_lines):
            self.WriteDataFileFromList('%s_infeasible%s.dat' % (name_prefix,
                                                                index_string),
                                       infeasible_lines)
            plot_parts.append('\'%s_infeasible%s.dat\' w p lt rgb "dark-red" pt 7' %
                              (name_prefix, index_string))
        if len(pregnant_lines):
            self.WriteDataFileFromList('%s_pregnant%s.dat' % (name_prefix,
                                                              index_string),
                                       pregnant_lines)
            plot_parts.append('\'%s_pregnant%s.dat\' w p lt rgb "green" pt 7' %
                              (name_prefix, index_string))
        if len(candidate_lines):
            for line in candidate_lines:
                plot_parts.append('"< echo %s" w p lt rgb "green" pt 7'
                                  %line.rstrip('\r\n'))
        if len(integer_lines):
            self.WriteDataFileFromList('%s_integer%s.dat' % (name_prefix,
                                                             index_string),
                                       integer_lines)
            plot_parts.append('\'%s_integer%s.dat\' w p lt rgb "cyan" pt 7' %
                              (name_prefix, index_string))
        if self._incumbent_value is not None:
            plot_parts.append('%0.6f lt 1 lw 0.5' % self._incumbent_value)
        additional_script_lines.append('plot %s\n' % ', '.join(plot_parts))
        additional_script_lines.append('unset arrow\n')
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._min_objective_value, self._max_objective_value)
        data = ''
        data += 'set terminal png notransparent size 480,360\n'
        data += 'set nokey\n'
        data += 'set autoscale\n'
        data += 'set tics scale 0.001\n'
        data += 'set pointsize 0.5\n'
        data += 'set xrange [-0.1:1.1]\n'
        data += 'set yrange [%0.6f:%0.6f]\n' % (image_max_obj,
                                                          image_min_obj)
        data += 'set format x ""\n'
        data += 'set ylabel "obj. value"\n'
        if self._filename is None:
            data += 'set title "B&B tree"\n\n'
        else:
            data += 'set title "B&B tree (%s %.2fs %s)"\n\n' % (
                self._filename, self._time, self._label)
        for line in additional_script_lines:
            data += line

        return data

    def ProcessLine(self, line):
        """
        Process a line of the input file, generating images if appropriate.
        Parses the line, updates internal data structures, and creates images
        if appropriate.
        Args:
          line: String input line to process.
        """
        line = line.strip()
        # Comments start with a '#'
        if line[0] == '#':
            return
        tokens = line.split()
        if len(tokens) < 3:
            print('Incomplete or invalid line: %s' %' '.join(tokens))
            sys.exit(1)
        # Tokens shared by all line types
        self._time = float(tokens[0])
        line_type = tokens[1]
        remaining_tokens = tokens[2:]
        # Process the line based on the type
        if line_type == 'heuristic':
            self._optimal_soln_time = self._time
            self.ProcessHeuristicLine(remaining_tokens)
        else:
            # Other node types share common tokens
            node_id = int(tokens[2])
            parent_id = int(tokens[3])
            branch_direction = tokens[4]
            remaining_tokens = tokens[5:]
            # TODO(aykut):parent id of root node is 0 when we read from file.
            if id==self.root:
                parent_id = None
            # Check that the parent node id is valid
            # elif parent_id not in self.get_node_list() and self.root is not None:
            #     print 'Parent id does not exist: %s' % line
            #     sys.exit(1)
            if line_type == 'integer':
                self._optimal_soln_time = self._time
                self.ProcessIntegerLine(node_id, parent_id,
                                        branch_direction, remaining_tokens)
            elif line_type == 'fathomed':
                self.ProcessFathomedLine(node_id, parent_id,
                                         branch_direction, remaining_tokens)
            elif line_type == 'candidate':
                self.ProcessCandidateLine(node_id, parent_id,
                                          branch_direction, remaining_tokens)
            elif line_type == 'pregnant':
                self.ProcessPregnantLine(node_id, parent_id,
                                         branch_direction, remaining_tokens)
            elif line_type == 'branched':
                self.ProcessBranchedLine(node_id, parent_id,
                                         branch_direction, remaining_tokens)
            elif line_type == 'infeasible':
                self.ProcessInfeasibleLine(node_id, parent_id,
                                           branch_direction, remaining_tokens)
            else:
                print('Unexpected line type "%s": %s' % (line_type,
                                                         ' '.join(tokens)))
                sys.exit(1)

    def ProcessHeuristicLine(self, remaining_tokens):
        """
        Core processing for a line of type 'heuristic'.
        Args:
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) < 1 or len(remaining_tokens) > 2:
            print('Invalid line: %s heuristic %s' % (
                    self._time, ' '.join(remaining_tokens)))
            print('Should match: <time> heuristic <obj value>'+\
                ' [<associated node id>]')
            sys.exit(1)
        objective_value = float(remaining_tokens[0])
        if len(remaining_tokens) == 2:
            associated_node = remaining_tokens[1]
        else:
            associated_node = None
        # Check that this is actually an improvement
        if self._incumbent_value is not None and self._optimization_sense is None:
            if objective_value > self._incumbent_value:
                print("Objective sense unset, guessing maximization")
                self._optimization_sense = 'max'
            else:
                print("Objective sense unset, guessing minimization")
                self._optimization_sense = 'min'
        if not self.IsBetterThanIncumbent(objective_value):
            return
        self._previous_incumbent_value = self._incumbent_value
        self._incumbent_value = objective_value
        self.UpdateObjectiveValueLimits(objective_value)
        self._incumbent_parent = associated_node
        # Set variable to generate images
        self._new_integer_solution = True

    def ProcessIntegerLine(self, node_id, parent_id, branch_direction,
                           remaining_tokens):
        """
        Core processing for a line of type 'integer'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node
          is the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) != 1:
            print('Invalid line: %s integer %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> integer <node id> <parent id>'+\
                '<branch direction> <obj value>')
            sys.exit(1)
        objective_value = float(remaining_tokens[0])
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'integer',
                             objective_value, None, None)
        self._previous_incumbent_value = self._incumbent_value
        self._incumbent_value = objective_value
        self._incumbent_parent = parent_id
        self._new_integer_solution = True

    def ProcessFathomedLine(self, node_id, parent_id, branch_direction,
                            remaining_tokens):
        """
        Core processing for a line of type 'fathomed'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node is
            the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Print a warning if there is no current incumbent.
        if self._incumbent_value is None:
            print('WARNING: Encountered "fathom" line before first incumbent.')
            print('  This may indicate an error in the input file.')
        # Parse remaining tokens
        if len(remaining_tokens) > 1:
            print('Invalid line: %s fathomed %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> fathomed <node id> <parent id>'+\
                '<branch direction> [<lp bound>]')
            sys.exit(1)
        if len(remaining_tokens) == 1:
            lp_bound = float(remaining_tokens[0])
        else:
            if (node_id in self.get_node_list() and
                self.get_node_attr(node_id, 'lp_bound') is not None):
                lp_bound = self.get_node_attr(node_id, 'lp_bound')
            else:
                lp_bound = self.get_node_attr(parent_id, 'lp_bound')
            if self._optimization_sense == 'min':
                if (self._incumbent_value is not None and
                    lp_bound < self._incumbent_value):
                    lp_bound = self._incumbent_value
            elif self._optimization_sense == 'max':
                if (self._incumbent_value is not None and
                    lp_bound > self._incumbent_value):
                    lp_bound = self._incumbent_value
        parent_node = self.get_node(parent_id)
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'fathomed',
                             lp_bound,
                             self.get_node_attr(parent_id,
                                                'integer_infeasibility_count'),
                             self.get_node_attr(parent_id,
                                                'integer_infeasibility_sum'))

    def ProcessPregnantLine(self, node_id, parent_id, branch_direction,
                            remaining_tokens):
        """
        Core processing for a line of type 'pregnant'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node is
            the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) != 3:
            print('Invalid line: %s pregnant %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> pregnant <node id> <parent id> ')
            print('<branch direction> <lp bound> ')
            print('<sum of integer infeasibilities> <number of integer ')
            print('infeasibilities>')
            sys.exit(1)
        lp_bound = float(remaining_tokens[0])
        integer_infeasibility_sum = float(remaining_tokens[1])
        integer_infeasibility_count = int(remaining_tokens[2])

        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'pregnant',
                             lp_bound, integer_infeasibility_count,
                             integer_infeasibility_sum)

    def ProcessBranchedLine(self, node_id, parent_id, branch_direction,
                            remaining_tokens):
        """
        Core processing for a line of type 'branched'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node
          is the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) not in [3, 5]:
            print('Invalid line: %s branched %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> branched <node id> <parent id> ')
            print('<branch direction> <lp bound> ')
            print('<sum of integer infeasibilities> <number of integer ')
            print('infeasibilities>')
            sys.exit(1)
        lp_bound = float(remaining_tokens[0])
        integer_infeasibility_sum = float(remaining_tokens[1])
        integer_infeasibility_count = int(remaining_tokens[2])
        condition_begin = None
        condition_end = None
        if len(remaining_tokens) == 5:
            # In this case, we must also be printing conditions numbers
            condition_begin = int(remaining_tokens[3])
            condition_end = int(remaining_tokens[4])
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'branched',
                             lp_bound, integer_infeasibility_count,
                             integer_infeasibility_sum, condition_begin,
                             condition_end)

    def ProcessInfeasibleLine(self, node_id, parent_id, branch_direction,
                              remaining_tokens):
        """
        Core processing for a line of type 'infeasible'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node is
            the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) not in [0, 2]:
            print('Invalid line: %s infeasible %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> infeasible <node id> <parent id> ')
            print('<branch direction>')
            sys.exit(1)
        # Use parent values if the node does not have its own
        lp_bound = self.get_node_attr(parent_id, 'lp_bound')
        ii_count = self.get_node_attr(parent_id, 'integer_infeasibility_count')
        ii_sum = self.get_node_attr(parent_id, 'integer_infeasibility_sum')
        if node_id in self.get_node_list():
            if self.get_node_attr(node_id, 'lp_bound') is not None:
                lp_bound = self.get_node_attr(node_id, 'lp_bound')
            if (self.get_node_attr(node_id, 'integer_infeasibility_count')
                is not None):
                ii_count = self.get_node_attr(node_id,
                                              'integer_infeasibility_count')
            if (self.get_node_attr(node_id, 'integer_infeasibility_sum')
                is not None):
                ii_sum = self.get_node_attr(node_id,'integer_infeasibility_sum')
        if len(remaining_tokens) == 2:
            # In this case, we must also be printing conditions numbers
            condition_begin = int(remaining_tokens[0])
            condition_end = int(remaining_tokens[1])
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'infeasible',
                             lp_bound, ii_count, ii_sum)

    def ProcessCandidateLine(self, node_id, parent_id, branch_direction,
                             remaining_tokens):
        """
        Core processing for a line of type 'candidate'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node
          is the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) == 2 or len(remaining_tokens) > 3:
            print('Invalid line: %s branched %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> candidate <node id> <parent id> ')
            print('<branch direction> [<lp bound>] ')
            print('[<sum of integer infeasibilities> <number of integer ')
            print('infeasibilities>]')
            sys.exit(1)
        # if parent_id not in self.get_node_list():
        #     print 'Error: node %s not in set' % parent_id
        #     sys.exit(1)
        # TODO(bhunsaker): Check that we handle the cases of updating a
        #candidate.
        if len(remaining_tokens) > 0:
            lp_bound = float(remaining_tokens[0])
        else:
            lp_bound = self.get_node_attr(parent_id, 'lp_bound')
        if len(remaining_tokens) == 3:
            integer_infeasibility_sum = float(remaining_tokens[1])
            integer_infeasibility_count = int(remaining_tokens[2])
        else:
            integer_infeasibility_sum = self.get_node_attr(parent_id,
                                                  'integer_infeasibility_sum')
            integer_infeasibility_count = self.get_node_attr(parent_id,
                                                'integer_infeasibility_count')
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'candidate',
                             lp_bound, integer_infeasibility_count,
                             integer_infeasibility_sum)

    def RunGnuplotOnAllFiles(self):
        """Runs Gnuplot on all files in self._gnuplot_files."""
        for file in self._gnuplot_files:
            subprocess.call(['gnuplot', file])

    def CreateAnimatedImages(self):
        """Create animated images based on the static images."""
        histogram_re = re.compile('histogram')
        histogram_images = [re.sub('gnuplot', 'png', file)
                            for file in self._gnuplot_files
                            if histogram_re.match(file)]
        if len(histogram_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(histogram_images)
            args.append('animated_histogram.gif')
            subprocess.call(args)
        scatterplot_re = re.compile('scatterplot')
        scatterplot_images = [re.sub('gnuplot', 'png', file)
                              for file in self._gnuplot_files
                              if scatterplot_re.match(file)]
        if len(scatterplot_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(scatterplot_images)
            args.append('animated_scatterplot.gif')
            subprocess.call(args)
        tree_re = re.compile('tree\.')
        tree_images = [re.sub('gnuplot', 'png', file)
                       for file in self._gnuplot_files
                       if tree_re.match(file)]
        if len(tree_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(tree_images)
            args.append('animated_tree.gif')
            subprocess.call(args)
        tree_alt_re = re.compile('tree_alt')
        tree_alt_images = [re.sub('gnuplot', 'png', file)
                           for file in self._gnuplot_files
                           if tree_alt_re.match(file)]
        if len(tree_alt_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(tree_alt_images)
            args.append('animated_tree_alt.gif')
            subprocess.call(args)

    def GeneratePredictionImages(self):
        gap_measures = self._objective_gap_forecaster.GetAllMeasures()
        ssg_measures = self._sum_subtree_gaps_forecaster.GetAllMeasures()
        # Check that there are values to process.
        if len(gap_measures) == 0 or len(ssg_measures) == 0:
            print('WARNING: Not printing prediction images because at least'+\
                ' one measure set is empty.')
            print('  Gap measures: %d' % len(gap_measures))
            print('  SSG measures: %d' % len(ssg_measures))
            return
        # Gap measures
        gap_data_filename = 'gap_measures.dat'
        data_file = open(gap_data_filename, 'w')
        for measure in gap_measures:
            data_file.write('%0.6f %0.6f\n' % (measure.time, measure.value))
        data_file.close()
        # SSG measures
        ssg_data_filename = 'ssg_measures.dat'
        data_file = open(ssg_data_filename, 'w')
        # We need to scale the SSG measures so that it will make sense to
        # look at them on the same plot with gap measures.
        scale_factor=old_div(float(gap_measures[0].value),float(ssg_measures[0].value))
        for measure in ssg_measures:
            data_file.write('%0.6f %0.6f\n' % (measure.time,
                                               measure.value * scale_factor))
        data_file.close()
        # Set terminal for the output files.
        measures_script = 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the plot.
        if self.filename is None:
            measures_script += 'set title "Progress Measures"\n'
        else:
            measures_script += ('set title "Progress Measures: %s, %s"\n' % (
                    self._filename, self._label))
        measures_script += 'set xlabel \"time (s)\"\n'
        measures_script += 'set ylabel \"measure\"\n'
        measures_script += 'set autoscale\n'
        # Plot the data points.
        measures_script += (
            'plot \'%s\' with linespoints linetype 3 title \"(SSG)\", '
            '\'%s\' with linespoints linetype 4 pointtype 19 '
            'title \"(MIP gap)\"\n' %
            (ssg_data_filename, gap_data_filename))
        measures_script += 'show output\n'

        return measures_script

    def GenerateForecastImages(self):
        # Forecasts
        # Gap forecasts
        gap_forecasts = self._objective_gap_forecaster.GetAllForecasts()
        gap_data_filename = 'gap_forecasts.dat'
        if gap_forecasts:
            data_file = open(gap_data_filename, 'w')
            for forecast in gap_forecasts:
                data_file.write('%0.6f %0.6f\n' % (forecast.time,
                                                   forecast.forecast))
            data_file.close()
        # SSG forecasts
        ssg_forecasts = self._sum_subtree_gaps_forecaster.GetAllForecasts()
        ssg_data_filename = 'ssg_forecasts.dat'
        if ssg_forecasts:
            data_file = open(ssg_data_filename, 'w')
            for forecast in ssg_forecasts:
                data_file.write('%0.6f %0.6f\n' % (forecast.time,
                                                   forecast.forecast))
            data_file.close()
        if not gap_forecasts and not ssg_forecasts:
            print('No forecasts made, so not creating forecast image.')
            return
        # Set terminal for the output files.
        forecast_script = 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the plot.
        if self._filename is None:
            forecast_script += 'set title "Forecasts"\n'
        else:
            forecast_script += ('set title "Forecasts: %s, %s"\n' % (
                    self._filename, self._label))
        forecast_script += 'set xlabel \"time (s)\"\n'
        forecast_script += 'set ylabel \"prediction of total time\"\n'
        forecast_script += 'set autoscale\n'
        # Plot the data points and the unit-slope line (to show elapsed time).
        forecast_script += 'plot '
        if forecast_forecasts:
            forecast_script += ('\'%s\' with linespoints linetype 3 '
                              'title \"(SSG)\", ' % ssg_data_filename)
        if gap_forecasts:
            forecast_script += ('\'%s\' with linespoints linetype 4 pointtype 19 '
            'title \"(MIP gap)\", ' % gap_data_filename)
        forecast_script += 'x linetype 0 title \"elapsed time\"\n'
        forecast_script += 'show output\n'

        return forecast_script

    def _get_fh(self, path, mode='r'):
        '''
        Return a file handle for given path.
        Path can be a string or a file handle.
        Attempt to uncompress/compress files ending in '.gz' and '.bz2'.
        '''
        if self._is_string_like(path):
            if path.endswith('.gz'):
#               import gzip
#               fh = gzip.open(path,mode=mode)  # doesn't return real fh
                fh=os.popen("gzcat "+path) # probably not portable
            elif path.endswith('.bz2'):
#               import bz2
#               fh = bz2.BZ2File(path,mode=mode) # doesn't return real fh
                fh=os.popen("bzcat "+path) # probably not portable
            else:
                fh = file(path,mode=mode)
        elif hasattr(path, 'write'):
            # Note, mode of file handle is unchanged.
            fh = path
        else:
            raise TypeError('path must be a string or file handle.')
        return fh

    def _is_string_like(self, obj): # from John Hunter, types-free version
        try:
            obj + ''
        except (TypeError, ValueError):
            return False
        return True

def CreatePerlStyleBooleanFlag(parser, flag_text, variable_name, help_text):
    """
    Add two options to an optparse.OptionParser, one with a 'no' prefix.
    Two options are created.  One has the flag_text and one has 'no' prepended
    to the flag_text.  For example, --foo and --nofoo.  This is similar to a
    common style in Perl.
    Args:
      parser: optparse.OptionParser object.
      flag_text: String text for the flag.
      variable_name: String name of the variable to store the flag results.
      help_text: String that describes the flag.
    """
    parser.add_option('--' + flag_text,
                      action='store_true', dest=variable_name, default=False,
                      help=help_text)
    parser.add_option('--no' + flag_text,
                      action='store_false', dest=variable_name,
                      help='do not ' + flag_text)

def parse_options():
    ''' Parse arguments and flags'''
    usage_text = 'usage: %prog [options] <input file>'
    parser = optparse.OptionParser(usage=usage_text)

    parser.add_option('--interval', dest='interval', type='float',
                      default=10.0,
                      help='generate images every TIME seconds',
                      metavar='TIME')
    parser.add_option('--time_limit', dest='time_limit', type='float',
                      help='process at most TIME seconds of solver time',
                      metavar='TIME')
    parser.add_option('--label', dest='label', default='',
                      help='add LABEL to all images',
                      metavar='LABEL')
    parser.add_option('--use_common_bounds', dest='use_common_bounds',
                      action='store_true', default=False,
                      help='use the same bounds on all images')
    CreatePerlStyleBooleanFlag(parser, 'fathom', 'fathom',
                               'do an interval check for fathomed nodes')
    CreatePerlStyleBooleanFlag(parser, 'histogram', 'histogram',
                               'print histograms')
    CreatePerlStyleBooleanFlag(parser, 'scatterplot', 'scatterplot',
                               'print scatterplots')
    CreatePerlStyleBooleanFlag(parser, 'path', 'path',
                               'print scatterplot incumbent paths')
    CreatePerlStyleBooleanFlag(parser, 'tree', 'tree',
                               'print tree images')
    CreatePerlStyleBooleanFlag(parser, 'fixed_tree', 'fixed_tree',
                               'print tree images with fixed horizontal '
                               'positions')
    CreatePerlStyleBooleanFlag(parser, 'predictions', 'predictions',
                               'print time predictions')
    parser.add_option('--all', dest='all_images', action='store_true',
                      default=False, help='print all images')
    parser.add_option('--logscaley', dest='logscaley',
                      action='store_true', default=False,
                      help='use log scale for histogram sizes')
    parser.add_option('--animate', dest='animate',
                      action='store_true', default=False,
                      help='create animated GIF of each image set')
    parser.add_option('--no_run_gnuplot', dest='run_gnuplot',
                      action='store_false', default=True,
                      help='do not run Gnuplot on the generated files')
    parser.add_option('--edge_limit', dest='edge_limit', type='int',
                      default=1000000,
                      help='do not print edges in tree plots if more than '
                      'NUM nodes',
                      metavar='NUM')
    parser.add_option('--sample_tree', dest='sample_tree', type='int',
                      default=0,
                      help='use at most NUM nodes of each type in tree images;'
                      ' zero means no limit',
                      metavar='NUM')
    (options, args) = parser.parse_args()
    if (len(args) != 1):
        parser.print_usage()
        sys.exit(1)
    input_filename = args[0]
    if options.all_images:
        options.histogram = True
        options.scatterplot = True
        options.path = True
        options.tree = True
        options.fixed_tree = True
        options.predictions = True
    # Abort if no images chosen
    if (not options.histogram and not options.scatterplot and
        not options.path and not options.tree and not options.fixed_tree and
        not options.predictions):
        print('No image types specified so not processing.')
        parser.print_usage()
        sys.exit(1)
    # Bounds for incumbent paths will be undefined without scatterplots.
    if options.path:
        options.scatterplot = True
    # TODO(bhunsaker): Check whether Gnuplot can be accessed early.
    # TODO(bhunsaker): Check whether animation program can be accessed.
    if (len(args) != 1):
        parser.print_usage()
        sys.exit(1)
    if options.all_images:
        options.histogram = True
        options.scatterplot = True
        options.path = True
        options.tree = True
        options.fixed_tree = True
        options.predictions = True
    # Abort if no images chosen
    if (not options.histogram and not options.scatterplot and
        not options.path and not options.tree and not options.fixed_tree and
        not options.predictions):
        print('No image types specified so not processing.')
        sys.exit(1)
    # Bounds for incumbent paths will be undefined without scatterplots.
    if options.path:
        options.scatterplot = True
    return (input_filename, options)


if __name__ == '__main__':
    from .BranchAndBound import GenerateRandomMIP, BranchAndBound

    T = BBTree()
    #T.set_layout('dot2tex')
    #T.set_display_mode('file')
    T.set_display_mode('matplotlib')
    CONSTRAINTS, VARIABLES, OBJ, MAT, RHS = GenerateRandomMIP(rand_seed = 120)
    BranchAndBound(T, CONSTRAINTS, VARIABLES, OBJ, MAT, RHS,
                   branch_strategy = MOST_FRACTIONAL,
                   search_strategy = BEST_FIRST,
                   display_interval = 10000)

Functions

def CreatePerlStyleBooleanFlag(parser, flag_text, variable_name, help_text)

Add two options to an optparse.OptionParser, one with a 'no' prefix. Two options are created. One has the flag_text and one has 'no' prepended to the flag_text. For example, –foo and –nofoo. This is similar to a common style in Perl.

Args

parser
optparse.OptionParser object.
flag_text
String text for the flag.
variable_name
String name of the variable to store the flag results.
help_text
String that describes the flag.
Expand source code
def CreatePerlStyleBooleanFlag(parser, flag_text, variable_name, help_text):
    """
    Add two options to an optparse.OptionParser, one with a 'no' prefix.
    Two options are created.  One has the flag_text and one has 'no' prepended
    to the flag_text.  For example, --foo and --nofoo.  This is similar to a
    common style in Perl.
    Args:
      parser: optparse.OptionParser object.
      flag_text: String text for the flag.
      variable_name: String name of the variable to store the flag results.
      help_text: String that describes the flag.
    """
    parser.add_option('--' + flag_text,
                      action='store_true', dest=variable_name, default=False,
                      help=help_text)
    parser.add_option('--no' + flag_text,
                      action='store_false', dest=variable_name,
                      help='do not ' + flag_text)
def parse_options()

Parse arguments and flags

Expand source code
def parse_options():
    ''' Parse arguments and flags'''
    usage_text = 'usage: %prog [options] <input file>'
    parser = optparse.OptionParser(usage=usage_text)

    parser.add_option('--interval', dest='interval', type='float',
                      default=10.0,
                      help='generate images every TIME seconds',
                      metavar='TIME')
    parser.add_option('--time_limit', dest='time_limit', type='float',
                      help='process at most TIME seconds of solver time',
                      metavar='TIME')
    parser.add_option('--label', dest='label', default='',
                      help='add LABEL to all images',
                      metavar='LABEL')
    parser.add_option('--use_common_bounds', dest='use_common_bounds',
                      action='store_true', default=False,
                      help='use the same bounds on all images')
    CreatePerlStyleBooleanFlag(parser, 'fathom', 'fathom',
                               'do an interval check for fathomed nodes')
    CreatePerlStyleBooleanFlag(parser, 'histogram', 'histogram',
                               'print histograms')
    CreatePerlStyleBooleanFlag(parser, 'scatterplot', 'scatterplot',
                               'print scatterplots')
    CreatePerlStyleBooleanFlag(parser, 'path', 'path',
                               'print scatterplot incumbent paths')
    CreatePerlStyleBooleanFlag(parser, 'tree', 'tree',
                               'print tree images')
    CreatePerlStyleBooleanFlag(parser, 'fixed_tree', 'fixed_tree',
                               'print tree images with fixed horizontal '
                               'positions')
    CreatePerlStyleBooleanFlag(parser, 'predictions', 'predictions',
                               'print time predictions')
    parser.add_option('--all', dest='all_images', action='store_true',
                      default=False, help='print all images')
    parser.add_option('--logscaley', dest='logscaley',
                      action='store_true', default=False,
                      help='use log scale for histogram sizes')
    parser.add_option('--animate', dest='animate',
                      action='store_true', default=False,
                      help='create animated GIF of each image set')
    parser.add_option('--no_run_gnuplot', dest='run_gnuplot',
                      action='store_false', default=True,
                      help='do not run Gnuplot on the generated files')
    parser.add_option('--edge_limit', dest='edge_limit', type='int',
                      default=1000000,
                      help='do not print edges in tree plots if more than '
                      'NUM nodes',
                      metavar='NUM')
    parser.add_option('--sample_tree', dest='sample_tree', type='int',
                      default=0,
                      help='use at most NUM nodes of each type in tree images;'
                      ' zero means no limit',
                      metavar='NUM')
    (options, args) = parser.parse_args()
    if (len(args) != 1):
        parser.print_usage()
        sys.exit(1)
    input_filename = args[0]
    if options.all_images:
        options.histogram = True
        options.scatterplot = True
        options.path = True
        options.tree = True
        options.fixed_tree = True
        options.predictions = True
    # Abort if no images chosen
    if (not options.histogram and not options.scatterplot and
        not options.path and not options.tree and not options.fixed_tree and
        not options.predictions):
        print('No image types specified so not processing.')
        parser.print_usage()
        sys.exit(1)
    # Bounds for incumbent paths will be undefined without scatterplots.
    if options.path:
        options.scatterplot = True
    # TODO(bhunsaker): Check whether Gnuplot can be accessed early.
    # TODO(bhunsaker): Check whether animation program can be accessed.
    if (len(args) != 1):
        parser.print_usage()
        sys.exit(1)
    if options.all_images:
        options.histogram = True
        options.scatterplot = True
        options.path = True
        options.tree = True
        options.fixed_tree = True
        options.predictions = True
    # Abort if no images chosen
    if (not options.histogram and not options.scatterplot and
        not options.path and not options.tree and not options.fixed_tree and
        not options.predictions):
        print('No image types specified so not processing.')
        sys.exit(1)
    # Bounds for incumbent paths will be undefined without scatterplots.
    if options.path:
        options.scatterplot = True
    return (input_filename, options)

Classes

class BBTree (**attrs)

Methods to process and visualize information about a b&b tree. It can process an output file (in a specific format, see BAK project in COIN-OR) of a solver that has three information. See run.py in examples directory fot this use. Moreover it implements a branch and bound method that can solve binary programs (0-1 variables only) using PuLP as an LP solver. It provides different branching and searching strategies. See test_strategies.py in test directory.

This is the main class of GrUMPy. It inherits BinaryTree from GIMPy and keeps the entire branch-and-bound tree in self.

API: init(self, **attrs)

Description

Class constructor.

Input

attrs: Tree attributes in keyword arguments format. See Graph and Tree class for details.

Expand source code
class BBTree(BinaryTree):
    """
    Methods to process and visualize information about a b&b tree. It can
    process an output file (in a specific format, see BAK project in COIN-OR) of
    a solver that has three information. See run.py in examples directory fot
    this use. Moreover it implements a branch and bound method that can solve
    binary programs (0-1 variables only) using PuLP as an LP solver. It provides
    different branching and searching strategies. See test_strategies.py in test
    directory.

    This is the main class of GrUMPy. It inherits BinaryTree from GIMPy and
    keeps the entire branch-and-bound tree in self.
    """
    def __init__(self, **attrs):
        if not 'layout' in attrs:
            attrs['layout']= 'bak'
        BinaryTree.__init__(self, **attrs)
        # User-controlled constant values
        self._label = ''
        self._filename = None
        self._logscaley = False
        self._fathom = False
        self._wait_for_keypress = True
        self._edge_limit = 1000000
        # current time, updated each time we read a new line
        self._time = 0.0
        # use at most NUM nodes of each type in tree images; zero means no limit
        self._sample_tree = 0
        # Instance-dependent constant values
        self._optimization_sense = None
        self._start_time = None
        self._histogram_lower_bound = None
        self._histogram_upper_bound = None
        self._scatterplot_lower_bound = None
        self._scatterplot_upper_bound = None
        self._integer_infeasibility_lower_bound = None
        self._integer_infeasibility_upper_bound = None
        # Changing reference values
        self._image_counter = 0
        self._next_image_time = None
        self._incumbent_value = None
        self._incumbent_parent = None
        self._new_integer_solution = False
        self._max_objective_value = None
        self._min_objective_value = None
        self._max_integer_infeasibility_sum = None
        # List of incumbent path data files, for the all incumbent paths image
        self._incumbent_path_datafiles = []
        # Objects for measuring and predicting progress
        self._objective_gap_forecaster = ForecastingChainedSequences()
        self._sum_subtree_gaps_forecaster = ForecastingChainedSequences()
        self._sum_subtree_gaps_scale = 1.0
        self._previous_incumbent_value = None  # Only needed for SSG
        if 'display' in attrs:
            self.set_display_mode(attrs['display'])
        else:
            self.set_display_mode('off')

    def process_file(self, file_name):
        self._filename = file_name
        input_file = open(file_name, 'r')
        # Parse all the lines
        for line in input_file:
            self.ProcessLine(line)
            if self.root is not None:
                self.display()
        input_file.close()

    def write_as_dynamic_gexf(self, filename, mode = "Dot"):
        if not GEXF_INSTALLED:
            print('Gexf not installed. Exiting.')
            return
        if mode == 'Dot':
            try:
                gexf = Gexf("Mike O'Sullivan", "Dynamic graph file")
                graph = gexf.addGraph("directed", "dynamic", "Dynamic graph")
                objAtt = graph.addNodeAttribute("obj", "0.0", "float")
                currAtt = graph.addNodeAttribute("current", "1.0",
                                                 "integer", "dynamic")
                node_names = self.get_node_list()
                for name in node_names:
                    node = self.get_node(name)
                    if node.get("step") is None:
                        raise Exception("Node without step in BBTree",
                                        "node =", node)
                    curr_step = '%s' % node.get("step")
                    next_step = "%s" % (node.get("step") + 1)
                    n = graph.addNode(name, node.get_label(), start=curr_step)
                    if node.get("obj") is None:
                        raise Exception("Node without objective in BBTree",
                                        "node =", node)
                    n.addAttribute(objAtt, "%s" % node.get("obj"))
                    n.addAttribute(currAtt, "1", start=curr_step, end=next_step)
                    n.addAttribute(currAtt, "0", start=next_step)
                edge_names = self.get_edge_list()
                for i, (m_name, n_name) in enumerate(edge_names):
                    edge = self.get_edge(m_name, n_name)
                    if edge.get("step") is None:
                        raise Exception("Edge without step in BBTree",
                                        "edge =", (m_name, n_name))
                    curr_step = "%s" % edge.get("step")
                    graph.addEdge(i, m_name, n_name, start=curr_step)
                output_file = open(filename + ".gexf", "w")
                gexf.write(output_file)
            except Exception as e:
                print(e)
                print("No .gexf file created")
        else:
            raise Exception("Only Dot mode supported in write_as_dynamic_gexf")

    def set_display_mode(self, mode):
        if mode is 'off':
            self.attr['display'] = mode
        elif mode is 'matplotlib':
            if MATPLOTLIB_INSTALLED:
                self.attr['display'] = 'matplotlib'
            else:
                print('Matplotlib is not installed. Display is set to off.')
                self.attr['display'] = 'off'
        elif mode is 'PIL':
            if PIL_INSTALLED:
                self.attr['display'] = 'PIL'
            else:
                print('PIL is not installed. Display is set to off.')
                self.attr['display'] = 'off'
        elif mode is 'xdot':
            if XDOT_INSTALLED:
                self.attr['display'] = 'xdot'
            else:
                print('Xdot is not installed. Display is set to off.')
                self.attr['display'] = 'off'
        elif mode is 'file':
            self.attr['display'] = 'file'
        elif mode is 'matplotlib':
            self.attr['display'] = 'matplotlib'
        else:
            raise Exception('%s is not a valid display mode.' %mode)

    def display(self, item = 'all', basename = 'graph', format='png',
                count=None, pause=False, wait_for_click=True):
        '''
        Displays/Saves images requested. BranchAndBound method calls this method
        to visualize the branch and bound tree.
        '''
        if self.attr['layout'] != 'bak':
            if 'init_log_cond' in self.root.attr:
                max_log_cond = 0
                for n in list(self.nodes.values()):
                    if 'init_log_cond' in n.attr:
                        max_log_cond = max(n.attr['init_log_cond'], max_log_cond)
                for n in list(self.nodes.values()):
                    if 'init_log_cond' in n.attr:
                        log_begin = n.attr['init_log_cond']
                        log_end = n.attr['final_log_cond']
                        normalized_cond = (1-old_div(log_begin,max_log_cond))
                        color = str(hex(int(normalized_cond*256))[2:]) if normalized_cond >= .0625 else '0' + str(hex(int(normalized_cond*256))[2:])
                        n.attr['label'] = '%.0f \n %.0f' % (log_begin, log_end)
                        n.attr['color'] = '#' + color*3
                        n.attr['fillcolor'] = '#' + color*3
                        n.attr['style'] = 'filled'
                    else:
                        n.attr['label'] = ' '
            BinaryTree.display(self, pause = pause, wait_for_click = wait_for_click)
            return
        if self.attr['display'] is 'off':
            return
        if self.attr['display'] is 'matplotlib':
            gnuplot_script = None
            if item=='all':
                self.display_all()
            elif item=='tree':
                gnuplot_script = self.GenerateTreeImage()
            elif item=='scatterplot':
                gnuplot_script = self.GenerateScatterplot()
            elif item=='histogram':
                gnuplot_script = self.GenerateHistogram()
            elif item=='incumbent':
                gnuplot_script = self.GenerateIncumbentPath()
            elif item=='forecast':
                gnuplot_script = self.GenerateForecastImages()
            else:
                raise Exception('Unknown display() method argument %s' %item)
            if gnuplot_script is not None:
                self.display_image(gnuplot_script)
            # clean auxilary files.
            histogram_files = [f for f in os.listdir(".")
                               if f.startswith("histogram")]
            incumbent_files = [f for f in os.listdir(".")
                               if f.startswith("incumbentpath")]
            scatterplot_files = [f for f in os.listdir(".")
                                 if f.startswith("scatterplot")]
            t_fathomed_files = [f for f in os.listdir(".")
                                if f.startswith("tree_fathomed")]
            t_infeasible_files = [f for f in os.listdir(".")
                                if f.startswith("tree_infeasible")]
            t_pregnant_files = [f for f in os.listdir(".")
                                if f.startswith("tree_pregnant")]
            t_integer_files = [f for f in os.listdir(".")
                               if f.startswith("tree_integer")]
            t_branched_files = [f for f in os.listdir(".")
                               if f.startswith("tree_branched")]
            bak_filelist = (histogram_files + incumbent_files +
                            scatterplot_files + t_fathomed_files +
                            t_integer_files + t_branched_files +
                            t_infeasible_files + t_pregnant_files)
            for f in bak_filelist:
                os.remove(f)
        elif self.attr['display'] is 'xdot':
            if XDOT_INSTALLED:
                window = xdot.DotWindow()
                window.set_dotcode(self.to_string())
                window.connect('destroy', gtk.main_quit)
                gtk.main()
            else:
                print('Error: xdot not installed. Display disabled.')
                self.attr['display'] = 'off'
        elif self.attr['display'] is 'file':
            if count is not None:
                basename = basename + '_' + str(count)
            if self.attr['layout'] is 'dot2tex':
                if DOT2TEX_INSTALLED:
                    if format != 'pdf' or format != 'ps':
                        print("Dot2tex only supports pdf and ps formats,"+\
                            "falling back to pdf")
                        format = 'pdf'
                    self.set_layout('dot')
                    tex = dot2tex.dot2tex(self.to_string(), autosize=True,
                                          texmode = 'math',
                                          template = DOT2TEX_TEMPLATE)
                    f = open(basename+'.tex', 'w')
                    f.write(tex)
                    f.close()
                    subprocess.call(['latex', basename])
                    if format == 'ps':
                        subprocess.call(['dvips', basename])
                    elif format == 'pdf':
                        subprocess.call(['pdflatex', basename])
                    self.set_layout('dot2tex')
                    # clean auxilary files.
                    aux_filelist = [basename+'.tex', basename+'.log',
                                    basename+'.dvi', basename+'.aux']
                    for f in aux_filelist:
                        os.remove(f)
                else:
                    print("Dot2tex not installed, falling back to graphviz")
                    self.set_layout('dot')
                    self.write(basename+'.'+format, self.get_layout(), format)
            else:
                gnuplot_script = None
                if item=='all':
                    self.display_all()
                elif item=='tree':
                    gnuplot_script = self.GenerateTreeImage()
                elif item=='scatterplot':
                    gnuplot_script = self.GenerateScatterplot()
                elif item=='histogram':
                    gnuplot_script = self.GenerateHistogram()
                elif item=='incumbent':
                    gnuplot_script = self.GenerateIncumbentPath()
                elif item=='forecast':
                    gnuplot_script = self.GenerateForecastImages()
                else:
                    raise Exception('Unknown display() method argument %s' %item)
                if gnuplot_script is not None:
                    self.write_image(gnuplot_script, basename+'.'+format)
                # clean auxilary files.
                histogram_files = [f for f in os.listdir(".")
                                   if f.startswith("histogram")]
                incumbent_files = [f for f in os.listdir(".")
                                   if f.startswith("incumbentpath")]
                scatterplot_files = [f for f in os.listdir(".")
                                     if f.startswith("scatterplot")]
                t_fathomed_files = [f for f in os.listdir(".")
                                    if f.startswith("tree_fathomed")]
                t_infeasible_files = [f for f in os.listdir(".")
                                      if f.startswith("tree_infeasible")]
                t_pregnant_files = [f for f in os.listdir(".")
                                    if f.startswith("tree_pregnant")]
                t_integer_files = [f for f in os.listdir(".")
                                   if f.startswith("tree_integer")]
                t_branched_files = [f for f in os.listdir(".")
                                    if f.startswith("tree_branched")]
                bak_filelist = (histogram_files + incumbent_files +
                                scatterplot_files + t_fathomed_files +
                                t_integer_files + t_branched_files +
                                t_infeasible_files + t_pregnant_files)
                for f in bak_filelist:
                    os.remove(f)
        else:
            raise Exception('Unknown display mode %s' %self.attr['display'])

    def display_all(self):
        '''
        Assumes all the images have the same size.
        '''
        print ('This function is deprected and no longer functions')
        return

        # Old source, just in case
        # if not (MATPLOTLIB_INSTALLED and PIL_INSTALLED:
        #     print('Matplotlib or Pillow not installed. Display disabled')
        #     return
        # tree = self.GenerateTreeImage()
        # scatterplot = self.GenerateScatterplot()
        # histogram = self.GenerateHistogram()
        # incumbent = self.GenerateIncumbentPath()
        # if tree is not None:
        #     imTree = StringIO(tree)
        #     pTree = pygame.image.load(imTree, 'png')
        #     sTree = pTree.get_size()
        #     rTree = pygame.Rect(0,0,sTree[0],sTree[1])
        # if scatterplot is not None:
        #     imScatterplot = StringIO(scatterplot)
        #     pScatterplot = pygame.image.load(imScatterplot, 'png')
        #     sScatterplot = pScatterplot.get_size()
        #     rScatterplot = pygame.Rect(sTree[0],0,sScatterplot[0],
        #                                sScatterplot[1])
        # if histogram is not None:
        #     imHistogram = StringIO(histogram)
        #     pHistogram = pygame.image.load(imHistogram, 'png')
        #     sHistogram = pHistogram.get_size()
        #     rHistogram = pygame.Rect(0,sTree[1],sHistogram[0],sHistogram[1])
        # if incumbent is not None:
        #     imIncumbent = StringIO(incumbent)
        #     pIncumbent = pygame.image.load(imIncumbent, 'png')
        #     sIncumbent = pIncumbent.get_size()
        #     rIncumbent = pygame.Rect(sTree[0],sTree[1],sIncumbent[0],
        #                              sIncumbent[1])
        # screen = pygame.display.set_mode((sTree[0]+sTree[0], sTree[1]+sTree[1]))
        # if tree is not None:
        #     screen.blit(pTree, rTree)
        # if scatterplot is not None:
        #     screen.blit(pScatterplot, rScatterplot)
        # if histogram is not None:
        #     screen.blit(pHistogram, rHistogram)
        # if incumbent is not None:
        #     screen.blit(pIncumbent, rIncumbent)
        # pygame.display.flip()
        # if self._wait_for_keypress:
        #     pause = True
        #     print("Press any key to continue to next image (ESCAPE to disable pausing)")
        # else:
        #     pause = False
        # while pause:
        #     e = pygame.event.poll()
        #     if e.type == pygame.KEYDOWN:
        #         keystate = pygame.key.get_pressed()
        #         if keystate[pygame.K_ESCAPE] != 0:
        #             self._wait_for_keypress = False
        #         break
        #     if e.type == pygame.QUIT:
        #         pause = False
        #         pygame.display.quit()
        #         # sys.exit() exits the whole program and I (aykut) guess it is
        #         # not appropriate here.
        #         #sys.exit()

    def write_image(self, gnuplot_script, filename):
        if not (MATPLOTLIB_INSTALLED and PIL_INSTALLED):
            print('Either matplotlib or Pillow is not installed. Display disabled')
            return
        try:
            p = subprocess.run(['gnuplot'], capture_output = True,
                               input = bytearray(gnuplot_script, 'utf8'))
        except OSError:
            print('''Gnuplot executable not found.
Gnuplot must be installed and in your search path.
After installation, ensure that the PATH variable is properly set.''')
            return
        p.check_returncode()

        if p.stderr:
            print(p.stderr)        

        if isinstance(filename, str):
            with open(filename, "w+b") as f:
                f.write(p.stdout)
        else:
            filename.write(p.stdout)

    def display_image(self, gnuplot_script, pause = False, wait_for_click = True):

        if not (PIL_INSTALLED and MATPLOTLIB_INSTALLED):
            print('Warning: Either matplotlib or Pillow is not installed. Cannot display.')
            return
        
        tmp_fd, tmp_name = tempfile.mkstemp()
        tmp_file = os.fdopen(tmp_fd, 'w+b')
        self.write_image(gnuplot_script, tmp_file)
        tmp_file.close()
        im = PIL_Image.open(tmp_name)
        plt.figure(1)
        plt.clf()
        plt.axis('off')
        plt.imshow(im, interpolation='bilinear' #resample=True
                   #extent = (0, 100, 0, 100)
        )
        if wait_for_click == True:
            plt.draw()
            try:
                if plt.waitforbuttonpress(timeout = 10000):
                    plt.close()
                    exit()
            except:
                exit()
        else:
            plt.show(block=pause)
        im.close()

    def set_label(self, label):
        self._label = label

    def set_logscaley(self, boolean):
        self._logscaley = boolean

    def set_fathom(self, boolean):
        self._fathom = boolean

    def set_edge_limit(self, limit):
        self._edge_limit = limit

    def set_sample_tree(self, number):
        self._sample_tree = number

    def AddOrUpdateNode(self, id, parent_id, branch_direction, status, lp_bound,
                        integer_infeasibility_count, integer_infeasibility_sum,
                        condition_begin = None, condition_end = None,
                        **attrs):
        '''
        This method designed to update nodes (in BAK) but we use it for
        updating/adding arcs. This is because of the tree data structure the
        authors adopted in BAK.
        We can divide these attributes such that some will belong to the edge
        parent_id->id and the others belong to the id node. The following shows
        whether the attribute belongs to edge or node.
        branch direction -> edge
        status -> node
        lp_bound -> node
        integer_infeasibility_count -> node
        integer_infeasibility_sum -> node
        parent_id -> node
        '''
        if (condition_begin is not None) and (condition_end is not None):
            #Figure out the color
            attrs['init_log_cond'] = math.log(condition_begin, 10)
            attrs['final_log_cond'] = math.log(condition_end, 10)
        if id in self.neighbors:
            # node already exists, update attributes
            self.set_node_attr(id, 'status', status)
            self.set_node_attr(id, 'lp_bound', lp_bound)
            self.set_node_attr(id, 'integer_infeasibility_count',
                               integer_infeasibility_count)
            self.set_node_attr(id, 'integer_infeasibility_sum',
                               integer_infeasibility_sum)
            if (condition_begin is not None) and (condition_end is not None):
                self.set_node_attr(id, 'init_log_cond',
                                   math.log(condition_begin, 10))
                self.set_node_attr(id, 'final_log_cond',
                                   math.log(condition_end, 10))
        elif self.root is None:
            self.add_root(id, status = status, lp_bound = lp_bound,
                          integer_infeasibility_count = integer_infeasibility_count,
                          integer_infeasibility_sum = integer_infeasibility_sum,
                          subtree_root = None, **attrs)
        elif parent_id is not None:
            if branch_direction == 'L':
                self.add_left_child(id, parent_id, status = status,
                    lp_bound = lp_bound,
                    integer_infeasibility_count = integer_infeasibility_count,
                    integer_infeasibility_sum = integer_infeasibility_sum,
                    subtree_root = None, **attrs)
            elif branch_direction == 'R':
                self.add_right_child(id, parent_id, status = status,
                    lp_bound = lp_bound,
                    integer_infeasibility_count = integer_infeasibility_count,
                    integer_infeasibility_sum = integer_infeasibility_sum,
                    subtree_root = None, **attrs)
        else:
            print('this should not happen.')
            raise Exception()
        if lp_bound is not None:
            self.UpdateObjectiveValueLimits(lp_bound)
            # Set optimization sense if not yet set
            if self._optimization_sense is None:
                if lp_bound < self.root.get_attr('lp_bound'):
                    self._optimization_sense = 'max'
                elif lp_bound > self.root.get_attr('lp_bound'):
                    self._optimization_sense = 'min'
            if self._optimization_sense == 'min' and lp_bound < self.root.get_attr('lp_bound'):
                print("Switching guess about objective sense to maximization based on bound change")
                self._optimization_sense = 'max'
            if self._optimization_sense == 'max' and lp_bound > self.root.get_attr('lp_bound'):
                print("Switching guess about objective sense to minimization based on bound change")
                self._optimization_sense = 'max'
        if integer_infeasibility_sum is not None:
            if (self._max_integer_infeasibility_sum is None or
                integer_infeasibility_sum >
                self._max_integer_infeasibility_sum):
                self._max_integer_infeasibility_sum = integer_infeasibility_sum

    def IsBetterThan(self, value1, value2):
        """
        Returns True if value1 is better than value2 as an objective value.
        This depends on the optimization sense of the instance.
        Args:
          value1: Float.
          value2: Float.
        Returns:
          True if value1 is better than value2 as an objective value.
        """
        if self._optimization_sense is None:
            print("Optimization sense is not set, assuming sense is miniminzation")
            self._optimization_sense = 'min'
        if self._optimization_sense == 'min':
            return value1 < value2
        else:
            return value1 > value2

    def IsBetterThanIncumbent(self, value):
        """
        Returns True if the passed value is better than current incumbent.
        Args:
          value: Float to use for comparison.
        Returns:
          True if the passed value is better than the current incumbent.
          'Better' is determined by the sense of optimization.
        """
        if self._incumbent_value is None:
            return True
        else:
            return self.IsBetterThan(value, self._incumbent_value)

    def UpdateObjectiveValueLimits(self, value):
        """Updates the min and max objective values if appropriate.
        Args:
          value: Float objective value.
        """
        if self._max_objective_value is None:
            self._max_objective_value = value
            self._min_objective_value = value
        else:
            if value > self._max_objective_value:
                self._max_objective_value = value
            if value < self._min_objective_value:
                self._min_objective_value = value

    def AddProgressMeasures(self):
        # No progress measures if there is no incumbent yet
        if self._incumbent_value is None:
            return
        # Store sum-of-subtree-gaps
        # We need to traverse all nodes unfortunately
        # TODO(bhunsaker): check whether we can just traverse active nodes
        active_node_count = 0
        subtree_bounds = {}
        new_integer_ssg = 0  # Only needed if this is a new integer solution
        for node_id in self.get_node_list():
            status = self.get_node_attr(node_id, 'status')
            if status == 'candidate' or status == 'pregnant':
                lp_bound = self.get_node_attr(node_id, 'lp_bound')
                subtree_root = self.get_node_attr(node_id, 'subtree_root')
                # Optional check for fathomed nodes.
                if (self._fathom and
                    not self.IsBetterThanIncumbent(lp_bound)):
                    continue
                active_node_count += 1
                if (subtree_root not in subtree_bounds or
                    self.IsBetterThan(lp_bound, subtree_bounds[subtree_root])):
                    subtree_bounds[subtree_root] = lp_bound
                if self._new_integer_solution:
                    self.set_node_attr(node_id, 'subtree_root', id)
                    new_integer_ssg += abs(self._incumbent_value - lp_bound)
        # If we have a new integer solution, we need to compute what
        # the measure would be with the previous integer solution for
        # scaling purposes.
        if (self._new_integer_solution and
            self._previous_incumbent_value is not None):
            reference_value = self._previous_incumbent_value
        else:
            reference_value = self._incumbent_value
        sum_subtree_gaps = 0
        for lp_bound in list(subtree_bounds.values()):
            sum_subtree_gaps += abs(reference_value - lp_bound)
        # Start a new sequence if a new integer solution was just found
        if self._new_integer_solution:
            if new_integer_ssg >= 1e-6:
                scale_factor = (old_div(float(sum_subtree_gaps),
                                float(new_integer_ssg)))
            else:
                scale_factor = 1.0
            self._sum_subtree_gaps_forecaster.StartNewSequence(scale_factor)
            # sum_subtree_gaps was based on the previous integer solution;
            # update it now
            sum_subtree_gaps = new_integer_ssg
        self._sum_subtree_gaps_forecaster.AddMeasure(self._time,
                                                     sum_subtree_gaps,
                                                     active_node_count,
                                                     len(self.get_node_list()))
        # Add objective gap measure.  Note that this relies on the
        # active_node_count computed above.
        if self._new_integer_solution:
            self._objective_gap_forecaster.StartNewSequence(1.0)
        if self._optimization_sense == 'min':
            obj_gap = self._incumbent_value - self._min_objective_value
        else:
            obj_gap = self._max_objective_value - self._incumbent_value
        self._objective_gap_forecaster.AddMeasure(self._time, obj_gap,
                                                  active_node_count,
                                                  len(self.get_node_list()))

    def GetImageCounterString(self):
        """
        Returns a string with the image counter.
        """
        return '%03d' % self._image_counter

    def WriteHistogramScript(self, num_bins, bin_width, max_bin_count,
                                 lp_bound, data_filename, output_file):
        """
        Write a Gnuplot script file to generate a histogram image.
        Args:
          num_bins: Integer number of bins for the histogram.
          bin_width: Float width of the bins in terms of objective values.
          max_bin_count: Integer number of the highest bin count.
          lp_bound: Float value of the current LP bound.
          data_filename: String name of the file; used for display purposes.
        """
        # TODO(bhunsaker): add checks for bin_width zero
        if self._incumbent_value is not None:
            incumbent_bin = 1 + ((self._incumbent_value -
                                  self._histogram_lower_bound) // bin_width)
            incumbent_x_coord = 0.5 + (old_div((self._incumbent_value -
                                        self._histogram_lower_bound),
                                       bin_width))
        lp_bound_bin = 1 + ((lp_bound - self._histogram_lower_bound) //
                            bin_width)
        lp_bound_x_coord = 0.5 + (old_div((lp_bound - self._histogram_lower_bound),
                                  bin_width))
        # TODO(bhunsaker): Ask Osman about adjust_xcoord option, which appears
        #    to put the vertical lines at the edge of bins rather than the
        #    true location.
        # Output the Gnuplot script to a file.
        script = ""
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the scatter plot.
        index_string = self.GetImageCounterString()
        output_filename = "histogram."+index_string+".png"
        if output_file:
            script += 'set output "%s"\n' % output_filename
        if self._filename is None:
            script += 'set title "Histogram of LP Bounds"\n'
        else:
            script += ('set title "Histogram of LP Bounds: %s, %s, %.2fs"\n'
                       % (self._filename, self._label, self._time))
        script += 'set xlabel "obj. value"\n'
        script += 'set ylabel "number of nodes"\n'
        if self._logscaley:
            script += 'set logscale y\n'
        script += 'set nokey\n'
        script += 'set tics scale 0.001\n'

        script += 'set xrange [0:%d+1]\n' % num_bins
        if self._logscaley:
            script += 'set yrange [1:%d*1.2]\n' % max_bin_count
        else:
            script += 'set yrange [0:%d*1.2]\n' % max_bin_count
        script += 'set xtics rotate by 90\n'
        # Mark tics for each bin.
        script += 'set xtics ('
        # TODO(bhunsaker): Consider putting this in a loop.
        x_values = ['"%0.2f" %0.2f' %
                    (self._histogram_lower_bound + i * bin_width, i + 0.5)
                    for i in range(num_bins + 1)]
        script += ', '.join(x_values) + ')\n'
        # Plot LP bound and incumbent tics.
        script += 'set x2tics ('
        script += '"%0.2f" %d' % (lp_bound, lp_bound_bin)
        if self._incumbent_value is not None:
            script += ', "%0.2f"%d)\n' % (self._incumbent_value,
                                                  incumbent_bin)
        else:
            script += ')\n'
        plot_parts = []
        # Plot the data points.
        plot_parts.append('\'%s\' with boxes fill solid 0.2' % data_filename)
        # Draw the vertical lp_bound and incumbent lines.
        script += 'set parametric\n'
        script += 'set trange [0:%d*1.5]\n' % max_bin_count
        plot_parts.append('%0.2f,t linetype 2' % lp_bound_x_coord)
        if self._incumbent_value is not None:
            plot_parts.append('%0.2f,t linetype 5' % incumbent_x_coord)
        script += 'plot %s\n' % ', '.join(plot_parts)
        script += 'unset parametric\n'
        script += 'show output\n'
        return script

    def AdjustHistogramEndBins(self, objective_list, num_bins, bin_width,
                               bin_counts, bin_centers, bin_widths):
        """
        Adjusts the two end bins if necessary to make them narrower.
        The two end bins may need to be narrower than the other bins so that
        they do not go past the current incumbent value on one end and the
        current lp bound on the other.  So that the histogram is still correct
        in areas, the height of these bins needs to be adjusted so that the
        area does not change.

        Note that there is likely to be some bias toward taller bins on
        the ends since they always have a point at one end of their width.  It
        may be more accurate visually to ignore or discount that one point when
        determining the bin height, but that is not currently done.

        Args:
          objective_list: List of float objective values.
          num_bins: Integer number of bins.
          bin_width: Float standard width of bins in terms of objective values.
          bin_counts: List of integer counts for each bin.
          bin_centers: List of float coordinates for the center of each bin.
          bin_widths: List of float widths for bins, allowing for individualized
            widths.
        """
        if self._optimization_sense == 'min':
            lp_bound = min(objective_list)
            lower_bound = lp_bound
            if self._incumbent_value is not None:
                upper_bound = self._incumbent_value
            else:
                upper_bound = self._histogram_upper_bound
        else:
            lp_bound = max(objective_list)
            upper_bound = lp_bound
            if self._incumbent_value is not None:
                lower_bound = self._incumbent_value
            else:
                lower_bound = self._histogram_lower_bound
        # The end bins may have unusual centers and widths
        highest_nonempty_bin = int((upper_bound -
                                    self._histogram_lower_bound) // bin_width)
        if (highest_nonempty_bin < num_bins and
            bin_counts[highest_nonempty_bin] > 0):
            highest_x_coord = 0.5 + (old_div((upper_bound -
                                      self._histogram_lower_bound), bin_width))
            highest_nonempty_bin_width, unused_int = math.modf(0.5 +
                                                               highest_x_coord)
            if highest_nonempty_bin_width == 0.0:
                highest_nonempty_bin_width = 1.0
            bin_widths[highest_nonempty_bin] = highest_nonempty_bin_width
            bin_centers[highest_nonempty_bin] = highest_x_coord - (
                old_div(highest_nonempty_bin_width, 2))
            # Scale the height appropriately
            bin_counts[highest_nonempty_bin] /= bin_widths[highest_nonempty_bin]
        lowest_nonempty_bin = int((lower_bound -
                                   self._histogram_lower_bound) // bin_width)
        if bin_counts[lowest_nonempty_bin] > 0:
            lowest_x_coord = 0.5 + (old_div((lower_bound -
                                     self._histogram_lower_bound), bin_width))
            lowest_nonempty_bin_excess, unused_int = math.modf(0.5 +
                                                               lowest_x_coord)
            bin_widths[lowest_nonempty_bin] = 1.0 - lowest_nonempty_bin_excess
            bin_centers[lowest_nonempty_bin] = (
                lowest_x_coord + old_div(bin_widths[lowest_nonempty_bin], 2))
            # Scale the height appropriately
            bin_counts[lowest_nonempty_bin] /= bin_widths[lowest_nonempty_bin]

    def GenerateHistogram(self, output_file = False):
        """
        Generate files necessary for a histogram image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        Args:
          time: Float number of seconds since the start of optimization.
        """
        num_bins = 20
        # Compute the bin width and counts.
        objective_list = []
        for n in self.get_node_list():
            if (self.get_node_attr(n,'status') == 'candidate' or
                self.get_node_attr(n,'status') == 'pregnant'):
                lp_bound = self.get_node_attr(n,'lp_bound')
                if not self.IsBetterThanIncumbent(lp_bound):
                    continue
                objective_list.append(lp_bound)
        # TODO(aykut) added the following check, we need it since we generate
        # histograms real time
        # we can not generate histogram if we do not have upperl and lower
        #bounds
        if len(objective_list)==0 or self._incumbent_value is None:
            return None
        # The first time we create a histogram, set bounds for objective
        # values.
        # TODO(bhunsaker): Consider bounds; talk to Osman.
        if self._histogram_lower_bound is None:
            if self._optimization_sense == 'min':
                self._histogram_lower_bound = min(objective_list)
                if self._incumbent_value is not None:
                    self._histogram_upper_bound = self._incumbent_value
                else:
                    self._histogram_upper_bound = max(objective_list)
            else:
                self._histogram_upper_bound = max(objective_list)
                if self._incumbent_value is not None:
                    self._histogram_lower_bound = self._incumbent_value
                else:
                    self._histogram_lower_bound = min(objective_list)
        bin_width = old_div((self._histogram_upper_bound -
                     self._histogram_lower_bound), float(num_bins))
        bin_counts = [0.0 for i in range(num_bins)]
        for value in objective_list:
            bin = int(math.floor(old_div((value - self._histogram_lower_bound),
                                 bin_width)))
            # Special case for the largest value.
            if (value >= self._histogram_upper_bound and
                value < self._histogram_upper_bound + 1e-6):
                bin = num_bins - 1
            if bin < 0:
                return
            assert bin < num_bins, '%d (%f) !< %d (%f)' % (
                bin, value, num_bins, self._histogram_upper_bound)
            bin_counts[bin] += 1
        max_bin_count = max(bin_counts)
        bin_centers = [i + 1.0 for i in range(len(bin_counts))]
        bin_widths = [1.0 for i in range(len(bin_counts))]
        self.AdjustHistogramEndBins(objective_list, num_bins, bin_width,
                                    bin_counts, bin_centers, bin_widths)
        if self._optimization_sense == 'min':
            lp_bound = min(objective_list)
        else:
            lp_bound = max(objective_list)
        # Output the bin data to a file.
        index_string = self.GetImageCounterString()
        data_filename = 'histogram%s.dat' % index_string
        data_file = open(data_filename, 'w')
        for index in range(len(bin_counts)):
            data_file.write('%f %f %f\n' % (bin_centers[index],
                                            bin_counts[index],
                                            bin_widths[index]))
        data_file.close()
        histogram_script = self.WriteHistogramScript(num_bins, bin_width,
                           max_bin_count, lp_bound, data_filename, output_file)
        # TODO(bhunsaker): Temporary hack
        #   This allows the bounds to be reset until an incumbent is found.
        if self._incumbent_value is None:
            self._histogram_lower_bound = None
            self._histogram_upper_bound = None
        return histogram_script

    def GetImageObjectiveBounds(self, min_value, max_value):
        """
        Return min and max bounds to be used for images.
        Images should use bounds that are slightly wider than observed
        objective values.  Also, the special case of a single value must be
        handled.
        Args:
          min_value: Float minimum objective value.
          max_value: Float maximum objective value.
        Returns:
          A tuple of two float values (lower_bound, upper_bound).
        """
        obj_range = max_value - min_value
        if obj_range > 0:
            image_max_obj = max_value + 0.01 * obj_range
            image_min_obj = min_value - 0.01 * obj_range
        else:
            if max_value >= 0:
                image_max_obj = 1.01 * max_value
            else:
                image_max_obj = 0.99 * max_value
            if min_value >= 0:
                image_min_obj = 0.99 * min_value
            else:
                image_min_obj = 1.01 * min_value
        return (image_min_obj, image_max_obj)

    def WriteScatterplotScript(self, data_filename, output_file):
        """
        Write a Gnuplot script file to generate a scatterplot image.
        Args:
          data_filename: String name of the file; used for display purposes.
        """
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._scatterplot_lower_bound, self._scatterplot_upper_bound)
        index_string = self.GetImageCounterString()
        output_filename = "scatterplot."+index_string+".png"
        script = ""
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the scatter plot.
        if output_file:
            script += 'set output "%s"\n' % output_filename
        if self._filename is None:
            script += 'set title "Scatterplot"\n'
        else:
            script += ('set title "Scatterplot: %s, %s, %ds"\n' % (
                    self._filename, self._label, int(self._time)))
        script += 'set pointsize 0.8\n'
        script += 'set nokey\n'
        script += 'set xlabel \"sum of int. infeas.\"\n'
        script += 'set ylabel \"obj. value\"\n'
        script += ('set xrange [0:%0.6f+2]\n' %
                          self._max_integer_infeasibility_sum)
        script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                          image_max_obj))
        plot_parts = []
        # Plot the data points.
        plot_parts.append('\'%s\' with points pointtype 2 linetype 1' %
                          data_filename)
        # Also plot the incumbent line.
        if self._incumbent_value is not None:
            plot_parts.append('%0.6f linetype 2 linewidth 0.5' %
                              self._incumbent_value)
        # Plot the incumbent's parent if it's available.
        if self._incumbent_parent is not None:
            #incumbent_parent = self.get_node(self._incumbent_parent)
            plot_parts.append('"< echo %0.6f %0.6f" '
                              'with points pointtype 9 pointsize 1.2' %
                              (self.get_node_attr(self._incumbent_parent,
                                                  'integer_infeasibility_sum'),
                               self.get_node_attr(self._incumbent_parent,
                                                  'lp_bound')))
        script += 'plot %s\n' % ', '.join(plot_parts)
        script += 'show output\n'
        return script

    def GenerateScatterplot(self, output_file = False):
        """
        Generate files necessary for a scatterplot image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        Args:
            output_file: if not given the gnuplot image will not be written
        to disk but returned (to be displayed in matplotlib window)
        """
        # Output data points.
        index_string = self.GetImageCounterString()
        data_filename = 'scatterplot%s.dat' % index_string
        data_file = open(data_filename, 'w')
        if self._scatterplot_lower_bound is None:
            bounds = []
        # Write objective values and integer infeasibility sum information
        # for candidate and pregnant nodes.
        for node in self.get_node_list():
            status = self.get_node_attr(node, 'status')
            lp_bound = self.get_node_attr(node,'lp_bound')
            if status == 'candidate' or status == 'pregnant':
                # Optional check for fathomed nodes.
                if (self._fathom and
                    not self.IsBetterThanIncumbent(lp_bound)):
                    continue
                data_file.write('%0.6f %0.6f\n' % (
                        self.get_node_attr(node, 'integer_infeasibility_sum'),
                        lp_bound))
                # Set the image objective bounds the first image.
                if self._scatterplot_lower_bound is None:
                    bounds.append(lp_bound)
        data_file.close()
        if self._scatterplot_lower_bound is None:
            if len(bounds) <= 1:
                return None
            self._scatterplot_lower_bound = min(bounds)
            self._scatterplot_upper_bound = max(bounds)
            # The incumbent overrides a bound if present.
            if self._incumbent_value is not None:
                if self._optimization_sense == 'min':
                    self._scatterplot_upper_bound = self._incumbent_value
                else:
                    self._scatterplot_lower_bound = self._incumbent_value
        scatterplot_script = self.WriteScatterplotScript(data_filename,
                                                         output_file)
        return scatterplot_script

    def WriteIncumbentPathScript(self, data_filename):
        """
        Write a Gnuplot script file to generate an incumbent path image.
        Args:
          data_filename: String name of the file; used for display purposes.
        """
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._scatterplot_lower_bound, self._scatterplot_upper_bound)
        script = ''
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'
        if self._filename is None:
            script += 'set title "Incumbent path"\n'
        else:
            script += ('set title "Incumbent path (%s %.2fs %s)"\n' % (
                    self._filename, self._time, self._label))
        script += 'set pointsize 0.8\n'
        script += 'set nokey\n'
        script += 'set xlabel \"sum of int. infeas.\"\n'
        script += 'set ylabel \"obj. value\"\n'
        script += ('set xrange [0:%0.6f+2]\n' %
                          self._max_integer_infeasibility_sum)
        script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                          image_max_obj))
        # Plot the data points and connecting lines.
        script += ('plot \'%s\' with points pointtype 2, '
                          '\'%s\' with lines linetype 2\n' %
                          (data_filename, data_filename))
        script += 'show output\n'
        return script

    def WriteAllIncumbentPathsScript(self):
        """
        Return a Gnuplot script string to generate an incumbent path image.
        Args:
          data_filenames: List of string names of files.
        """
        data_filenames = self._incumbent_path_datafiles
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._scatterplot_lower_bound, self._scatterplot_upper_bound)
        script = ''
        # Set terminal for the output files.
        script += 'set terminal png notransparent size 480,360\n\n'

        # Make settings for the scatter plot.
        if self._filename is None:
            script += 'set title "Incumbent paths"\n'
        else:
            script += ('set title "Incumbent paths (%s %.2fs %s)"\n' % (
                    self._filename, self._time, self._label))
        script += 'set pointsize 0.8\n'
        script += 'set nokey\n'
        script += 'set xlabel \"sum of int. infeas.\"\n'
        script += 'set ylabel \"obj. value\"\n'
        script += ('set xrange [0:%0.6f+2]\n' %
                          self._max_integer_infeasibility_sum)
        script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                          image_max_obj))
        # Plot the data points and connecting lines.
        command_list = []
        for filename in data_filenames:
            command_list.append('\'%s\' with points pointtype 2, '
                                '\'%s\' with lines linetype 2' %
                                (filename, filename))
        script += 'plot %s\n' % ','.join(command_list)
        script += 'show output\n'
        return script

    def GenerateIncumbentPath(self):
        """
        Generate files necessary for an incumbent scatterplot path image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        """
        if self._incumbent_parent is None:
            return
        if self._scatterplot_lower_bound is None:
            return
        if self._scatterplot_upper_bound is None:
            return
        index_string = self.GetImageCounterString()
        # Output data points.
        data_filename = 'incumbentpath%s.dat' % index_string
        data_file = open(data_filename, 'w')
        # Write objective values and integer infeasibility sum information
        # for ancestor nodes.
        data_file.write('0 %0.6f\n' % self._incumbent_value)
        parent = self._incumbent_parent
        # TODO(bhunsaker): I think the following assumes a unique value for the
        #   parent of the root.
        while parent != None:
            data_file.write('%0.6f %0.6f\n'
                            % (self.get_node_attr(parent,
                               'integer_infeasibility_sum'),
                             self.get_node_attr(parent, 'lp_bound')))
            parent = self.get_node_attr(parent, 'parent')
        data_file.close()
        self._incumbent_path_datafiles.append(data_filename)
        # Output the Gnuplot script to a file.
        path_script = self.WriteIncumbentPathScript(data_filename)
        return path_script

    def GenerateAllIncumbentPaths(self):
        """
        Generate file for a path image with all incumbent paths.
        Data files were previously generated for each incumbent.  This re-uses
        those files.
        """
        all_path_script = self.WriteAllIncumbentPathsScript()

    def WriteTreeScript(self, additional_lines = None):
        """
        Write a Gnuplot script file to generate a tree image.
        Args:
          additional_lines: String with additional lines to be added to the
            script file.
        """
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._min_objective_value, self._max_objective_value)
        data = ''
        data += 'set terminal png notransparent size 480,360\n'
        data += 'set output "%s"\n' % output_file
        data += 'set nokey\n'
        data += 'set autoscale\n'
        data += 'set tics scale 0.001\n'
        data += 'set pointsize 0.5\n'
        data += 'set xrange [-0.1:1.1]\n'
        data += 'set yrange [%0.6f:%0.6f]\n' % (image_max_obj,
                                                          image_min_obj)
        data += 'set format x ""\n'
        data += 'set ylabel "obj. value"\n'
        if self._filename is None:
            data += 'set title "B&B tree"\n'
        else:
            data += 'set title "B&B tree (%s %.2fs %s)"\n\n' % (
                self._filename, self._time, self._label)
        for line in additional_lines:
            data += line
        return data

    def GetTreeFixedHorizontalPositions(self):
        """
        Returns horizontal positions for all nodes based on fixed positions.
        Returns:
          Dictionary of float horizontal positions, keyed by node id.
        """
        # Statistics needed for horizontal positions.
        horizontal_lower_bound = dict.fromkeys(self.get_node_list(), 0.0)
        horizontal_upper_bound = dict.fromkeys(self.get_node_list(), 1.0)
        horizontal_positions = dict.fromkeys(self.get_node_list())
        horizontal_positions[self.root.name] = 0.5
        # sort node list
        node_id_list = sorted(self.get_node_list())
        node_id_list_int = list(int(n) for n in node_id_list)
        node_id_list_int = sorted(node_id_list_int)
        node_id_list = list(str(n) for n in node_id_list_int)
        for node_id in node_id_list:
            if node_id == self.root.name:
                continue
            parent_id = self.get_node_attr(node_id, 'parent')
            branch_direction = self.get_node_attr(node_id, 'direction')
            if branch_direction == 'R':
                horizontal_lower_bound[node_id] = horizontal_positions[
                    parent_id]
                horizontal_upper_bound[node_id] = horizontal_upper_bound[
                    parent_id]
            elif branch_direction == 'L':
                horizontal_lower_bound[node_id] = horizontal_lower_bound[
                    parent_id]
                horizontal_upper_bound[node_id] = horizontal_positions[
                    parent_id]
            else:
                print('Error: node %s has unsupported branching direction.'\
                    %node_id)
                print('Fixed-position tree images only support L and R ')
                print('branching.')
                sys.exit(1)
            horizontal_positions[node_id] = old_div((
                horizontal_upper_bound[node_id] +
                horizontal_lower_bound[node_id]), 2)
        return horizontal_positions

    def GetTreeHorizontalPositions(self):
        """
        Returns horizontal positions for all nodes.
        Each node is given equal horizontal space.
        Returns:
          Dictionary of float horizontal positions, keyed by node id.
        """
        # Statistics needed for horizontal positions.
        number_descendants = dict.fromkeys(self.get_node_list(), 1)
        # number_descendants includes the key node itself
        horizontal_lower_bound = dict.fromkeys(self.get_node_list(), 0.0)
        horizontal_upper_bound = dict.fromkeys(self.get_node_list(), 1.0)
        horizontal_positions = dict.fromkeys(self.get_node_list())
        visited = dict.fromkeys(self.get_node_list(), False)
        # Count the number of descendants for each node.
        # Do a post-order traversal of the tree.
        node_stack = []
        node_stack.append(self.root.name)
        while node_stack:
            current_node = node_stack[len(node_stack) - 1]
            lchild = self.get_left_child(current_node)
            rchild = self.get_right_child(current_node)
            is_node_added = False
            # Add the next unvisited child to the stack
            if lchild is not None and not visited[quote(lchild)]:
                node_stack.append(lchild)
                is_node_added = True
            if (rchild is not None and not visited[quote(rchild)] and
                is_node_added==False):
                node_stack.append(rchild)
                is_node_added = True
            # If all childs visited, then update number_descendants
            if not is_node_added:
                if lchild is not None:
                    number_descendants[quote(current_node)] += (
                                number_descendants[quote(lchild)])
                if rchild is not None:
                    number_descendants[quote(current_node)] += (
                                number_descendants[quote(rchild)])
                visited[quote(current_node)] = True
                del node_stack[len(node_stack) - 1]
        # Traverse the tree and set horizontal positions.
        # Do a pre-order traversal of the tree.
        node_stack = []
        node_stack.append(self.root.name)
        horizontal_lower_bound[self.root.name] = 0.0
        horizontal_upper_bound[self.root.name] = 1.0
        while node_stack:
            node = node_stack.pop()
            lchild = self.get_left_child(node)
            rchild = self.get_right_child(node)
            direction = None
            number_of_children = 0
            children_list = []
            # Place all children on the stack
            if lchild is not None:
                node_stack.append(lchild)
                number_of_children += 1
                direction = 'L'
                children_list.append(lchild)
            if rchild is not None:
                node_stack.append(rchild)
                number_of_children += 1
                direction = 'R'
                children_list.append(rchild)
            # Convenience variables
            current_lower_bound = horizontal_lower_bound[quote(node)]
            current_range = (horizontal_upper_bound[quote(node)] -
                             horizontal_lower_bound[quote(node)])
            total_descendants = number_descendants[quote(node)]
            sorted_child_labels = sorted(children_list)
            # Determine where to place this node with respect to its children.
            # Put the node in the center, or have more children on the left.
            before_index = int(math.ceil(old_div(number_of_children,2.0)))
            # Exception with a single node that is 'L'
            if number_of_children == 1:
                if direction != 'L':
                    before_index = 0
            cumulative_descendants = 0
            for i, label in enumerate(sorted_child_labels):
                if before_index == i:
                    # Determine the relative position for the current node
                    relative_position = old_div((cumulative_descendants + 0.5), (
                        total_descendants))
                    cumulative_descendants += 1
                # Set bounds for this child
                horizontal_lower_bound[quote(label)] = (
                    current_lower_bound + float(cumulative_descendants) /
                    total_descendants * current_range)
                # Increment cumulative_descendants, which also lets us compute
                # the upper bound.
                cumulative_descendants += number_descendants[quote(label)]
                horizontal_upper_bound[quote(label)] = (
                    current_lower_bound + float(cumulative_descendants) /
                    total_descendants * current_range)
            # Catch the case that the node comes after all its children.
            # This must also work for the case that this is the only node.
            if before_index == len(sorted_child_labels):
                relative_position = old_div((cumulative_descendants + 0.5), (
                    total_descendants))
            # Finally set the position for the current node
            horizontal_positions[quote(node)] = (
                horizontal_lower_bound[quote(node)] + relative_position * (
                    horizontal_upper_bound[quote(node)] -
                    horizontal_lower_bound[quote(node)]))
        return horizontal_positions

    def WriteDataFileFromList(self, filename, data_list):
        """
        Write a list of string data to a file with one entry per line.
        Args:
          filename: String filename to open.
          data_list: List of string values to write.
        """
        outfile = open(filename, 'w')
        for line in data_list:
            outfile.write(line)
        outfile.close()

    def GenerateTreeImage(self, fixed_horizontal_positions = False):
        """
        Generate files necessary for a tree image.
        Two files are necessary: a data file and a Gnuplot script file (which
        references the data file).
        """
        index_string = self.GetImageCounterString()
        if fixed_horizontal_positions:
            name_prefix = 'tree_alt'
            horizontal_positions = self.GetTreeFixedHorizontalPositions()
        else:
            name_prefix = 'tree'
            horizontal_positions = self.GetTreeHorizontalPositions()
        candidate_lines = []
        pregnant_lines = []
        branched_lines = []
        infeasible_lines = []
        fathomed_lines = []
        integer_lines = []
        additional_script_lines = []
        node_list = self.get_node_list()
        print_edges = (len(node_list) <= self._edge_limit)
        for node in node_list:
            node_lp_bound = self.get_node_attr(node, 'lp_bound')
            if self.get_node_attr(node, 'status') == 'candidate':
                # TODO(bhunsaker): add optional fathoming check
                candidate_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'pregnant':
                # TODO(bhunsaker): add optional fathoming check
                pregnant_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'branched':
                branched_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'infeasible':
                infeasible_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'fathomed':
                fathomed_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            elif self.get_node_attr(node, 'status') == 'integer':
                integer_lines.append('%0.6f %0.6f\n' % (
                        horizontal_positions[node], node_lp_bound))
            if print_edges and node != self.root.name:
                if True:
                    _parent_id = self.get_node_attr(node, 'parent')
                    additional_script_lines.append(
                     'set arrow from %0.6f, %0.6f to %0.6f, %0.6f nohead lt -1 '
                     'lw 0.2\n' % (horizontal_positions[_parent_id],
                     self.get_node_attr(_parent_id, 'lp_bound'),
                     horizontal_positions[node],
                     self.get_node_attr(node, 'lp_bound')))
        plot_parts = []
        # Plot root node.
        plot_parts.append('"< echo %0.6f %0.6f" w p lt 2 pt 7' %
                          (horizontal_positions[self.root.name],
                           self.root.get_attr('lp_bound')))
        # If desired, sample from the set of nodes rather than plotting all.
        if self._sample_tree:
            sample_size = self._sample_tree
            if len(branched_lines) > sample_size:
                branched_lines = random.sample(branched_lines, sample_size)
            if len(fathomed_lines) > sample_size:
                fathomed_lines = random.sample(fathomed_lines, sample_size)
            if len(infeasible_lines) > sample_size:
                infeasible_lines = random.sample(infeasible_lines, sample_size)
            if len(pregnant_lines) > sample_size:
                pregnant_lines = random.sample(pregnant_lines, sample_size)
            if len(candidate_lines) > sample_size:
                candidate_lines = random.sample(candidate_lines, sample_size)
            if len(integer_lines) > sample_size:
                integer_lines = random.sample(integer_lines, sample_size)
        # Output all data files.  Note that the order below matters.
        if len(branched_lines):
            self.WriteDataFileFromList('%s_branched%s.dat' % (name_prefix,
                                                              index_string),
                                       branched_lines)
            plot_parts.append('\'%s_branched%s.dat\' w p lt rgb "yellow" pt 7' %
                              (name_prefix, index_string))
        if len(fathomed_lines):
            self.WriteDataFileFromList('%s_fathomed%s.dat' % (name_prefix,
                                                              index_string),
                                       fathomed_lines)
            plot_parts.append('\'%s_fathomed%s.dat\' w p lt rgb "light-red" pt 7' %
                              (name_prefix, index_string))
        if len(infeasible_lines):
            self.WriteDataFileFromList('%s_infeasible%s.dat' % (name_prefix,
                                                                index_string),
                                       infeasible_lines)
            plot_parts.append('\'%s_infeasible%s.dat\' w p lt rgb "dark-red" pt 7' %
                              (name_prefix, index_string))
        if len(pregnant_lines):
            self.WriteDataFileFromList('%s_pregnant%s.dat' % (name_prefix,
                                                              index_string),
                                       pregnant_lines)
            plot_parts.append('\'%s_pregnant%s.dat\' w p lt rgb "green" pt 7' %
                              (name_prefix, index_string))
        if len(candidate_lines):
            for line in candidate_lines:
                plot_parts.append('"< echo %s" w p lt rgb "green" pt 7'
                                  %line.rstrip('\r\n'))
        if len(integer_lines):
            self.WriteDataFileFromList('%s_integer%s.dat' % (name_prefix,
                                                             index_string),
                                       integer_lines)
            plot_parts.append('\'%s_integer%s.dat\' w p lt rgb "cyan" pt 7' %
                              (name_prefix, index_string))
        if self._incumbent_value is not None:
            plot_parts.append('%0.6f lt 1 lw 0.5' % self._incumbent_value)
        additional_script_lines.append('plot %s\n' % ', '.join(plot_parts))
        additional_script_lines.append('unset arrow\n')
        image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
            self._min_objective_value, self._max_objective_value)
        data = ''
        data += 'set terminal png notransparent size 480,360\n'
        data += 'set nokey\n'
        data += 'set autoscale\n'
        data += 'set tics scale 0.001\n'
        data += 'set pointsize 0.5\n'
        data += 'set xrange [-0.1:1.1]\n'
        data += 'set yrange [%0.6f:%0.6f]\n' % (image_max_obj,
                                                          image_min_obj)
        data += 'set format x ""\n'
        data += 'set ylabel "obj. value"\n'
        if self._filename is None:
            data += 'set title "B&B tree"\n\n'
        else:
            data += 'set title "B&B tree (%s %.2fs %s)"\n\n' % (
                self._filename, self._time, self._label)
        for line in additional_script_lines:
            data += line

        return data

    def ProcessLine(self, line):
        """
        Process a line of the input file, generating images if appropriate.
        Parses the line, updates internal data structures, and creates images
        if appropriate.
        Args:
          line: String input line to process.
        """
        line = line.strip()
        # Comments start with a '#'
        if line[0] == '#':
            return
        tokens = line.split()
        if len(tokens) < 3:
            print('Incomplete or invalid line: %s' %' '.join(tokens))
            sys.exit(1)
        # Tokens shared by all line types
        self._time = float(tokens[0])
        line_type = tokens[1]
        remaining_tokens = tokens[2:]
        # Process the line based on the type
        if line_type == 'heuristic':
            self._optimal_soln_time = self._time
            self.ProcessHeuristicLine(remaining_tokens)
        else:
            # Other node types share common tokens
            node_id = int(tokens[2])
            parent_id = int(tokens[3])
            branch_direction = tokens[4]
            remaining_tokens = tokens[5:]
            # TODO(aykut):parent id of root node is 0 when we read from file.
            if id==self.root:
                parent_id = None
            # Check that the parent node id is valid
            # elif parent_id not in self.get_node_list() and self.root is not None:
            #     print 'Parent id does not exist: %s' % line
            #     sys.exit(1)
            if line_type == 'integer':
                self._optimal_soln_time = self._time
                self.ProcessIntegerLine(node_id, parent_id,
                                        branch_direction, remaining_tokens)
            elif line_type == 'fathomed':
                self.ProcessFathomedLine(node_id, parent_id,
                                         branch_direction, remaining_tokens)
            elif line_type == 'candidate':
                self.ProcessCandidateLine(node_id, parent_id,
                                          branch_direction, remaining_tokens)
            elif line_type == 'pregnant':
                self.ProcessPregnantLine(node_id, parent_id,
                                         branch_direction, remaining_tokens)
            elif line_type == 'branched':
                self.ProcessBranchedLine(node_id, parent_id,
                                         branch_direction, remaining_tokens)
            elif line_type == 'infeasible':
                self.ProcessInfeasibleLine(node_id, parent_id,
                                           branch_direction, remaining_tokens)
            else:
                print('Unexpected line type "%s": %s' % (line_type,
                                                         ' '.join(tokens)))
                sys.exit(1)

    def ProcessHeuristicLine(self, remaining_tokens):
        """
        Core processing for a line of type 'heuristic'.
        Args:
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) < 1 or len(remaining_tokens) > 2:
            print('Invalid line: %s heuristic %s' % (
                    self._time, ' '.join(remaining_tokens)))
            print('Should match: <time> heuristic <obj value>'+\
                ' [<associated node id>]')
            sys.exit(1)
        objective_value = float(remaining_tokens[0])
        if len(remaining_tokens) == 2:
            associated_node = remaining_tokens[1]
        else:
            associated_node = None
        # Check that this is actually an improvement
        if self._incumbent_value is not None and self._optimization_sense is None:
            if objective_value > self._incumbent_value:
                print("Objective sense unset, guessing maximization")
                self._optimization_sense = 'max'
            else:
                print("Objective sense unset, guessing minimization")
                self._optimization_sense = 'min'
        if not self.IsBetterThanIncumbent(objective_value):
            return
        self._previous_incumbent_value = self._incumbent_value
        self._incumbent_value = objective_value
        self.UpdateObjectiveValueLimits(objective_value)
        self._incumbent_parent = associated_node
        # Set variable to generate images
        self._new_integer_solution = True

    def ProcessIntegerLine(self, node_id, parent_id, branch_direction,
                           remaining_tokens):
        """
        Core processing for a line of type 'integer'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node
          is the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) != 1:
            print('Invalid line: %s integer %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> integer <node id> <parent id>'+\
                '<branch direction> <obj value>')
            sys.exit(1)
        objective_value = float(remaining_tokens[0])
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'integer',
                             objective_value, None, None)
        self._previous_incumbent_value = self._incumbent_value
        self._incumbent_value = objective_value
        self._incumbent_parent = parent_id
        self._new_integer_solution = True

    def ProcessFathomedLine(self, node_id, parent_id, branch_direction,
                            remaining_tokens):
        """
        Core processing for a line of type 'fathomed'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node is
            the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Print a warning if there is no current incumbent.
        if self._incumbent_value is None:
            print('WARNING: Encountered "fathom" line before first incumbent.')
            print('  This may indicate an error in the input file.')
        # Parse remaining tokens
        if len(remaining_tokens) > 1:
            print('Invalid line: %s fathomed %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> fathomed <node id> <parent id>'+\
                '<branch direction> [<lp bound>]')
            sys.exit(1)
        if len(remaining_tokens) == 1:
            lp_bound = float(remaining_tokens[0])
        else:
            if (node_id in self.get_node_list() and
                self.get_node_attr(node_id, 'lp_bound') is not None):
                lp_bound = self.get_node_attr(node_id, 'lp_bound')
            else:
                lp_bound = self.get_node_attr(parent_id, 'lp_bound')
            if self._optimization_sense == 'min':
                if (self._incumbent_value is not None and
                    lp_bound < self._incumbent_value):
                    lp_bound = self._incumbent_value
            elif self._optimization_sense == 'max':
                if (self._incumbent_value is not None and
                    lp_bound > self._incumbent_value):
                    lp_bound = self._incumbent_value
        parent_node = self.get_node(parent_id)
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'fathomed',
                             lp_bound,
                             self.get_node_attr(parent_id,
                                                'integer_infeasibility_count'),
                             self.get_node_attr(parent_id,
                                                'integer_infeasibility_sum'))

    def ProcessPregnantLine(self, node_id, parent_id, branch_direction,
                            remaining_tokens):
        """
        Core processing for a line of type 'pregnant'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node is
            the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) != 3:
            print('Invalid line: %s pregnant %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> pregnant <node id> <parent id> ')
            print('<branch direction> <lp bound> ')
            print('<sum of integer infeasibilities> <number of integer ')
            print('infeasibilities>')
            sys.exit(1)
        lp_bound = float(remaining_tokens[0])
        integer_infeasibility_sum = float(remaining_tokens[1])
        integer_infeasibility_count = int(remaining_tokens[2])

        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'pregnant',
                             lp_bound, integer_infeasibility_count,
                             integer_infeasibility_sum)

    def ProcessBranchedLine(self, node_id, parent_id, branch_direction,
                            remaining_tokens):
        """
        Core processing for a line of type 'branched'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node
          is the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) not in [3, 5]:
            print('Invalid line: %s branched %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> branched <node id> <parent id> ')
            print('<branch direction> <lp bound> ')
            print('<sum of integer infeasibilities> <number of integer ')
            print('infeasibilities>')
            sys.exit(1)
        lp_bound = float(remaining_tokens[0])
        integer_infeasibility_sum = float(remaining_tokens[1])
        integer_infeasibility_count = int(remaining_tokens[2])
        condition_begin = None
        condition_end = None
        if len(remaining_tokens) == 5:
            # In this case, we must also be printing conditions numbers
            condition_begin = int(remaining_tokens[3])
            condition_end = int(remaining_tokens[4])
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'branched',
                             lp_bound, integer_infeasibility_count,
                             integer_infeasibility_sum, condition_begin,
                             condition_end)

    def ProcessInfeasibleLine(self, node_id, parent_id, branch_direction,
                              remaining_tokens):
        """
        Core processing for a line of type 'infeasible'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node is
            the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) not in [0, 2]:
            print('Invalid line: %s infeasible %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> infeasible <node id> <parent id> ')
            print('<branch direction>')
            sys.exit(1)
        # Use parent values if the node does not have its own
        lp_bound = self.get_node_attr(parent_id, 'lp_bound')
        ii_count = self.get_node_attr(parent_id, 'integer_infeasibility_count')
        ii_sum = self.get_node_attr(parent_id, 'integer_infeasibility_sum')
        if node_id in self.get_node_list():
            if self.get_node_attr(node_id, 'lp_bound') is not None:
                lp_bound = self.get_node_attr(node_id, 'lp_bound')
            if (self.get_node_attr(node_id, 'integer_infeasibility_count')
                is not None):
                ii_count = self.get_node_attr(node_id,
                                              'integer_infeasibility_count')
            if (self.get_node_attr(node_id, 'integer_infeasibility_sum')
                is not None):
                ii_sum = self.get_node_attr(node_id,'integer_infeasibility_sum')
        if len(remaining_tokens) == 2:
            # In this case, we must also be printing conditions numbers
            condition_begin = int(remaining_tokens[0])
            condition_end = int(remaining_tokens[1])
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'infeasible',
                             lp_bound, ii_count, ii_sum)

    def ProcessCandidateLine(self, node_id, parent_id, branch_direction,
                             remaining_tokens):
        """
        Core processing for a line of type 'candidate'.
        Args:
          node_id: String node id.
          parent_id: String node id of parent.
          branch_direction: String of 'L' or 'R' indicating whether this node
          is the left or right child of its parent.
          remaining_tokens: List of string tokens. These are those that remain
            after any common tokens are processed.
        """
        # Parse remaining tokens
        if len(remaining_tokens) == 2 or len(remaining_tokens) > 3:
            print('Invalid line: %s branched %s %s %s %s' % (
                    self._time, node_id, parent_id, branch_direction,
                    ' '.join(remaining_tokens)))
            print('Should match: <time> candidate <node id> <parent id> ')
            print('<branch direction> [<lp bound>] ')
            print('[<sum of integer infeasibilities> <number of integer ')
            print('infeasibilities>]')
            sys.exit(1)
        # if parent_id not in self.get_node_list():
        #     print 'Error: node %s not in set' % parent_id
        #     sys.exit(1)
        # TODO(bhunsaker): Check that we handle the cases of updating a
        #candidate.
        if len(remaining_tokens) > 0:
            lp_bound = float(remaining_tokens[0])
        else:
            lp_bound = self.get_node_attr(parent_id, 'lp_bound')
        if len(remaining_tokens) == 3:
            integer_infeasibility_sum = float(remaining_tokens[1])
            integer_infeasibility_count = int(remaining_tokens[2])
        else:
            integer_infeasibility_sum = self.get_node_attr(parent_id,
                                                  'integer_infeasibility_sum')
            integer_infeasibility_count = self.get_node_attr(parent_id,
                                                'integer_infeasibility_count')
        self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'candidate',
                             lp_bound, integer_infeasibility_count,
                             integer_infeasibility_sum)

    def RunGnuplotOnAllFiles(self):
        """Runs Gnuplot on all files in self._gnuplot_files."""
        for file in self._gnuplot_files:
            subprocess.call(['gnuplot', file])

    def CreateAnimatedImages(self):
        """Create animated images based on the static images."""
        histogram_re = re.compile('histogram')
        histogram_images = [re.sub('gnuplot', 'png', file)
                            for file in self._gnuplot_files
                            if histogram_re.match(file)]
        if len(histogram_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(histogram_images)
            args.append('animated_histogram.gif')
            subprocess.call(args)
        scatterplot_re = re.compile('scatterplot')
        scatterplot_images = [re.sub('gnuplot', 'png', file)
                              for file in self._gnuplot_files
                              if scatterplot_re.match(file)]
        if len(scatterplot_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(scatterplot_images)
            args.append('animated_scatterplot.gif')
            subprocess.call(args)
        tree_re = re.compile('tree\.')
        tree_images = [re.sub('gnuplot', 'png', file)
                       for file in self._gnuplot_files
                       if tree_re.match(file)]
        if len(tree_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(tree_images)
            args.append('animated_tree.gif')
            subprocess.call(args)
        tree_alt_re = re.compile('tree_alt')
        tree_alt_images = [re.sub('gnuplot', 'png', file)
                           for file in self._gnuplot_files
                           if tree_alt_re.match(file)]
        if len(tree_alt_images):
            args = ['convert', '-delay', '15', '-loop', '1']
            args.extend(tree_alt_images)
            args.append('animated_tree_alt.gif')
            subprocess.call(args)

    def GeneratePredictionImages(self):
        gap_measures = self._objective_gap_forecaster.GetAllMeasures()
        ssg_measures = self._sum_subtree_gaps_forecaster.GetAllMeasures()
        # Check that there are values to process.
        if len(gap_measures) == 0 or len(ssg_measures) == 0:
            print('WARNING: Not printing prediction images because at least'+\
                ' one measure set is empty.')
            print('  Gap measures: %d' % len(gap_measures))
            print('  SSG measures: %d' % len(ssg_measures))
            return
        # Gap measures
        gap_data_filename = 'gap_measures.dat'
        data_file = open(gap_data_filename, 'w')
        for measure in gap_measures:
            data_file.write('%0.6f %0.6f\n' % (measure.time, measure.value))
        data_file.close()
        # SSG measures
        ssg_data_filename = 'ssg_measures.dat'
        data_file = open(ssg_data_filename, 'w')
        # We need to scale the SSG measures so that it will make sense to
        # look at them on the same plot with gap measures.
        scale_factor=old_div(float(gap_measures[0].value),float(ssg_measures[0].value))
        for measure in ssg_measures:
            data_file.write('%0.6f %0.6f\n' % (measure.time,
                                               measure.value * scale_factor))
        data_file.close()
        # Set terminal for the output files.
        measures_script = 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the plot.
        if self.filename is None:
            measures_script += 'set title "Progress Measures"\n'
        else:
            measures_script += ('set title "Progress Measures: %s, %s"\n' % (
                    self._filename, self._label))
        measures_script += 'set xlabel \"time (s)\"\n'
        measures_script += 'set ylabel \"measure\"\n'
        measures_script += 'set autoscale\n'
        # Plot the data points.
        measures_script += (
            'plot \'%s\' with linespoints linetype 3 title \"(SSG)\", '
            '\'%s\' with linespoints linetype 4 pointtype 19 '
            'title \"(MIP gap)\"\n' %
            (ssg_data_filename, gap_data_filename))
        measures_script += 'show output\n'

        return measures_script

    def GenerateForecastImages(self):
        # Forecasts
        # Gap forecasts
        gap_forecasts = self._objective_gap_forecaster.GetAllForecasts()
        gap_data_filename = 'gap_forecasts.dat'
        if gap_forecasts:
            data_file = open(gap_data_filename, 'w')
            for forecast in gap_forecasts:
                data_file.write('%0.6f %0.6f\n' % (forecast.time,
                                                   forecast.forecast))
            data_file.close()
        # SSG forecasts
        ssg_forecasts = self._sum_subtree_gaps_forecaster.GetAllForecasts()
        ssg_data_filename = 'ssg_forecasts.dat'
        if ssg_forecasts:
            data_file = open(ssg_data_filename, 'w')
            for forecast in ssg_forecasts:
                data_file.write('%0.6f %0.6f\n' % (forecast.time,
                                                   forecast.forecast))
            data_file.close()
        if not gap_forecasts and not ssg_forecasts:
            print('No forecasts made, so not creating forecast image.')
            return
        # Set terminal for the output files.
        forecast_script = 'set terminal png notransparent size 480,360\n\n'
        # Make settings for the plot.
        if self._filename is None:
            forecast_script += 'set title "Forecasts"\n'
        else:
            forecast_script += ('set title "Forecasts: %s, %s"\n' % (
                    self._filename, self._label))
        forecast_script += 'set xlabel \"time (s)\"\n'
        forecast_script += 'set ylabel \"prediction of total time\"\n'
        forecast_script += 'set autoscale\n'
        # Plot the data points and the unit-slope line (to show elapsed time).
        forecast_script += 'plot '
        if forecast_forecasts:
            forecast_script += ('\'%s\' with linespoints linetype 3 '
                              'title \"(SSG)\", ' % ssg_data_filename)
        if gap_forecasts:
            forecast_script += ('\'%s\' with linespoints linetype 4 pointtype 19 '
            'title \"(MIP gap)\", ' % gap_data_filename)
        forecast_script += 'x linetype 0 title \"elapsed time\"\n'
        forecast_script += 'show output\n'

        return forecast_script

    def _get_fh(self, path, mode='r'):
        '''
        Return a file handle for given path.
        Path can be a string or a file handle.
        Attempt to uncompress/compress files ending in '.gz' and '.bz2'.
        '''
        if self._is_string_like(path):
            if path.endswith('.gz'):
#               import gzip
#               fh = gzip.open(path,mode=mode)  # doesn't return real fh
                fh=os.popen("gzcat "+path) # probably not portable
            elif path.endswith('.bz2'):
#               import bz2
#               fh = bz2.BZ2File(path,mode=mode) # doesn't return real fh
                fh=os.popen("bzcat "+path) # probably not portable
            else:
                fh = file(path,mode=mode)
        elif hasattr(path, 'write'):
            # Note, mode of file handle is unchanged.
            fh = path
        else:
            raise TypeError('path must be a string or file handle.')
        return fh

    def _is_string_like(self, obj): # from John Hunter, types-free version
        try:
            obj + ''
        except (TypeError, ValueError):
            return False
        return True

Ancestors

  • coinor.gimpy.tree.BinaryTree
  • coinor.gimpy.tree.Tree
  • coinor.gimpy.graph.Graph

Methods

def AddOrUpdateNode(self, id, parent_id, branch_direction, status, lp_bound, integer_infeasibility_count, integer_infeasibility_sum, condition_begin=None, condition_end=None, **attrs)

This method designed to update nodes (in BAK) but we use it for updating/adding arcs. This is because of the tree data structure the authors adopted in BAK. We can divide these attributes such that some will belong to the edge parent_id->id and the others belong to the id node. The following shows whether the attribute belongs to edge or node. branch direction -> edge status -> node lp_bound -> node integer_infeasibility_count -> node integer_infeasibility_sum -> node parent_id -> node

Expand source code
def AddOrUpdateNode(self, id, parent_id, branch_direction, status, lp_bound,
                    integer_infeasibility_count, integer_infeasibility_sum,
                    condition_begin = None, condition_end = None,
                    **attrs):
    '''
    This method designed to update nodes (in BAK) but we use it for
    updating/adding arcs. This is because of the tree data structure the
    authors adopted in BAK.
    We can divide these attributes such that some will belong to the edge
    parent_id->id and the others belong to the id node. The following shows
    whether the attribute belongs to edge or node.
    branch direction -> edge
    status -> node
    lp_bound -> node
    integer_infeasibility_count -> node
    integer_infeasibility_sum -> node
    parent_id -> node
    '''
    if (condition_begin is not None) and (condition_end is not None):
        #Figure out the color
        attrs['init_log_cond'] = math.log(condition_begin, 10)
        attrs['final_log_cond'] = math.log(condition_end, 10)
    if id in self.neighbors:
        # node already exists, update attributes
        self.set_node_attr(id, 'status', status)
        self.set_node_attr(id, 'lp_bound', lp_bound)
        self.set_node_attr(id, 'integer_infeasibility_count',
                           integer_infeasibility_count)
        self.set_node_attr(id, 'integer_infeasibility_sum',
                           integer_infeasibility_sum)
        if (condition_begin is not None) and (condition_end is not None):
            self.set_node_attr(id, 'init_log_cond',
                               math.log(condition_begin, 10))
            self.set_node_attr(id, 'final_log_cond',
                               math.log(condition_end, 10))
    elif self.root is None:
        self.add_root(id, status = status, lp_bound = lp_bound,
                      integer_infeasibility_count = integer_infeasibility_count,
                      integer_infeasibility_sum = integer_infeasibility_sum,
                      subtree_root = None, **attrs)
    elif parent_id is not None:
        if branch_direction == 'L':
            self.add_left_child(id, parent_id, status = status,
                lp_bound = lp_bound,
                integer_infeasibility_count = integer_infeasibility_count,
                integer_infeasibility_sum = integer_infeasibility_sum,
                subtree_root = None, **attrs)
        elif branch_direction == 'R':
            self.add_right_child(id, parent_id, status = status,
                lp_bound = lp_bound,
                integer_infeasibility_count = integer_infeasibility_count,
                integer_infeasibility_sum = integer_infeasibility_sum,
                subtree_root = None, **attrs)
    else:
        print('this should not happen.')
        raise Exception()
    if lp_bound is not None:
        self.UpdateObjectiveValueLimits(lp_bound)
        # Set optimization sense if not yet set
        if self._optimization_sense is None:
            if lp_bound < self.root.get_attr('lp_bound'):
                self._optimization_sense = 'max'
            elif lp_bound > self.root.get_attr('lp_bound'):
                self._optimization_sense = 'min'
        if self._optimization_sense == 'min' and lp_bound < self.root.get_attr('lp_bound'):
            print("Switching guess about objective sense to maximization based on bound change")
            self._optimization_sense = 'max'
        if self._optimization_sense == 'max' and lp_bound > self.root.get_attr('lp_bound'):
            print("Switching guess about objective sense to minimization based on bound change")
            self._optimization_sense = 'max'
    if integer_infeasibility_sum is not None:
        if (self._max_integer_infeasibility_sum is None or
            integer_infeasibility_sum >
            self._max_integer_infeasibility_sum):
            self._max_integer_infeasibility_sum = integer_infeasibility_sum
def AddProgressMeasures(self)
Expand source code
def AddProgressMeasures(self):
    # No progress measures if there is no incumbent yet
    if self._incumbent_value is None:
        return
    # Store sum-of-subtree-gaps
    # We need to traverse all nodes unfortunately
    # TODO(bhunsaker): check whether we can just traverse active nodes
    active_node_count = 0
    subtree_bounds = {}
    new_integer_ssg = 0  # Only needed if this is a new integer solution
    for node_id in self.get_node_list():
        status = self.get_node_attr(node_id, 'status')
        if status == 'candidate' or status == 'pregnant':
            lp_bound = self.get_node_attr(node_id, 'lp_bound')
            subtree_root = self.get_node_attr(node_id, 'subtree_root')
            # Optional check for fathomed nodes.
            if (self._fathom and
                not self.IsBetterThanIncumbent(lp_bound)):
                continue
            active_node_count += 1
            if (subtree_root not in subtree_bounds or
                self.IsBetterThan(lp_bound, subtree_bounds[subtree_root])):
                subtree_bounds[subtree_root] = lp_bound
            if self._new_integer_solution:
                self.set_node_attr(node_id, 'subtree_root', id)
                new_integer_ssg += abs(self._incumbent_value - lp_bound)
    # If we have a new integer solution, we need to compute what
    # the measure would be with the previous integer solution for
    # scaling purposes.
    if (self._new_integer_solution and
        self._previous_incumbent_value is not None):
        reference_value = self._previous_incumbent_value
    else:
        reference_value = self._incumbent_value
    sum_subtree_gaps = 0
    for lp_bound in list(subtree_bounds.values()):
        sum_subtree_gaps += abs(reference_value - lp_bound)
    # Start a new sequence if a new integer solution was just found
    if self._new_integer_solution:
        if new_integer_ssg >= 1e-6:
            scale_factor = (old_div(float(sum_subtree_gaps),
                            float(new_integer_ssg)))
        else:
            scale_factor = 1.0
        self._sum_subtree_gaps_forecaster.StartNewSequence(scale_factor)
        # sum_subtree_gaps was based on the previous integer solution;
        # update it now
        sum_subtree_gaps = new_integer_ssg
    self._sum_subtree_gaps_forecaster.AddMeasure(self._time,
                                                 sum_subtree_gaps,
                                                 active_node_count,
                                                 len(self.get_node_list()))
    # Add objective gap measure.  Note that this relies on the
    # active_node_count computed above.
    if self._new_integer_solution:
        self._objective_gap_forecaster.StartNewSequence(1.0)
    if self._optimization_sense == 'min':
        obj_gap = self._incumbent_value - self._min_objective_value
    else:
        obj_gap = self._max_objective_value - self._incumbent_value
    self._objective_gap_forecaster.AddMeasure(self._time, obj_gap,
                                              active_node_count,
                                              len(self.get_node_list()))
def AdjustHistogramEndBins(self, objective_list, num_bins, bin_width, bin_counts, bin_centers, bin_widths)

Adjusts the two end bins if necessary to make them narrower. The two end bins may need to be narrower than the other bins so that they do not go past the current incumbent value on one end and the current lp bound on the other. So that the histogram is still correct in areas, the height of these bins needs to be adjusted so that the area does not change.

Note that there is likely to be some bias toward taller bins on the ends since they always have a point at one end of their width. It may be more accurate visually to ignore or discount that one point when determining the bin height, but that is not currently done.

Args

objective_list
List of float objective values.
num_bins
Integer number of bins.
bin_width
Float standard width of bins in terms of objective values.
bin_counts
List of integer counts for each bin.
bin_centers
List of float coordinates for the center of each bin.
bin_widths
List of float widths for bins, allowing for individualized widths.
Expand source code
def AdjustHistogramEndBins(self, objective_list, num_bins, bin_width,
                           bin_counts, bin_centers, bin_widths):
    """
    Adjusts the two end bins if necessary to make them narrower.
    The two end bins may need to be narrower than the other bins so that
    they do not go past the current incumbent value on one end and the
    current lp bound on the other.  So that the histogram is still correct
    in areas, the height of these bins needs to be adjusted so that the
    area does not change.

    Note that there is likely to be some bias toward taller bins on
    the ends since they always have a point at one end of their width.  It
    may be more accurate visually to ignore or discount that one point when
    determining the bin height, but that is not currently done.

    Args:
      objective_list: List of float objective values.
      num_bins: Integer number of bins.
      bin_width: Float standard width of bins in terms of objective values.
      bin_counts: List of integer counts for each bin.
      bin_centers: List of float coordinates for the center of each bin.
      bin_widths: List of float widths for bins, allowing for individualized
        widths.
    """
    if self._optimization_sense == 'min':
        lp_bound = min(objective_list)
        lower_bound = lp_bound
        if self._incumbent_value is not None:
            upper_bound = self._incumbent_value
        else:
            upper_bound = self._histogram_upper_bound
    else:
        lp_bound = max(objective_list)
        upper_bound = lp_bound
        if self._incumbent_value is not None:
            lower_bound = self._incumbent_value
        else:
            lower_bound = self._histogram_lower_bound
    # The end bins may have unusual centers and widths
    highest_nonempty_bin = int((upper_bound -
                                self._histogram_lower_bound) // bin_width)
    if (highest_nonempty_bin < num_bins and
        bin_counts[highest_nonempty_bin] > 0):
        highest_x_coord = 0.5 + (old_div((upper_bound -
                                  self._histogram_lower_bound), bin_width))
        highest_nonempty_bin_width, unused_int = math.modf(0.5 +
                                                           highest_x_coord)
        if highest_nonempty_bin_width == 0.0:
            highest_nonempty_bin_width = 1.0
        bin_widths[highest_nonempty_bin] = highest_nonempty_bin_width
        bin_centers[highest_nonempty_bin] = highest_x_coord - (
            old_div(highest_nonempty_bin_width, 2))
        # Scale the height appropriately
        bin_counts[highest_nonempty_bin] /= bin_widths[highest_nonempty_bin]
    lowest_nonempty_bin = int((lower_bound -
                               self._histogram_lower_bound) // bin_width)
    if bin_counts[lowest_nonempty_bin] > 0:
        lowest_x_coord = 0.5 + (old_div((lower_bound -
                                 self._histogram_lower_bound), bin_width))
        lowest_nonempty_bin_excess, unused_int = math.modf(0.5 +
                                                           lowest_x_coord)
        bin_widths[lowest_nonempty_bin] = 1.0 - lowest_nonempty_bin_excess
        bin_centers[lowest_nonempty_bin] = (
            lowest_x_coord + old_div(bin_widths[lowest_nonempty_bin], 2))
        # Scale the height appropriately
        bin_counts[lowest_nonempty_bin] /= bin_widths[lowest_nonempty_bin]
def CreateAnimatedImages(self)

Create animated images based on the static images.

Expand source code
def CreateAnimatedImages(self):
    """Create animated images based on the static images."""
    histogram_re = re.compile('histogram')
    histogram_images = [re.sub('gnuplot', 'png', file)
                        for file in self._gnuplot_files
                        if histogram_re.match(file)]
    if len(histogram_images):
        args = ['convert', '-delay', '15', '-loop', '1']
        args.extend(histogram_images)
        args.append('animated_histogram.gif')
        subprocess.call(args)
    scatterplot_re = re.compile('scatterplot')
    scatterplot_images = [re.sub('gnuplot', 'png', file)
                          for file in self._gnuplot_files
                          if scatterplot_re.match(file)]
    if len(scatterplot_images):
        args = ['convert', '-delay', '15', '-loop', '1']
        args.extend(scatterplot_images)
        args.append('animated_scatterplot.gif')
        subprocess.call(args)
    tree_re = re.compile('tree\.')
    tree_images = [re.sub('gnuplot', 'png', file)
                   for file in self._gnuplot_files
                   if tree_re.match(file)]
    if len(tree_images):
        args = ['convert', '-delay', '15', '-loop', '1']
        args.extend(tree_images)
        args.append('animated_tree.gif')
        subprocess.call(args)
    tree_alt_re = re.compile('tree_alt')
    tree_alt_images = [re.sub('gnuplot', 'png', file)
                       for file in self._gnuplot_files
                       if tree_alt_re.match(file)]
    if len(tree_alt_images):
        args = ['convert', '-delay', '15', '-loop', '1']
        args.extend(tree_alt_images)
        args.append('animated_tree_alt.gif')
        subprocess.call(args)
def GenerateAllIncumbentPaths(self)

Generate file for a path image with all incumbent paths. Data files were previously generated for each incumbent. This re-uses those files.

Expand source code
def GenerateAllIncumbentPaths(self):
    """
    Generate file for a path image with all incumbent paths.
    Data files were previously generated for each incumbent.  This re-uses
    those files.
    """
    all_path_script = self.WriteAllIncumbentPathsScript()
def GenerateForecastImages(self)
Expand source code
def GenerateForecastImages(self):
    # Forecasts
    # Gap forecasts
    gap_forecasts = self._objective_gap_forecaster.GetAllForecasts()
    gap_data_filename = 'gap_forecasts.dat'
    if gap_forecasts:
        data_file = open(gap_data_filename, 'w')
        for forecast in gap_forecasts:
            data_file.write('%0.6f %0.6f\n' % (forecast.time,
                                               forecast.forecast))
        data_file.close()
    # SSG forecasts
    ssg_forecasts = self._sum_subtree_gaps_forecaster.GetAllForecasts()
    ssg_data_filename = 'ssg_forecasts.dat'
    if ssg_forecasts:
        data_file = open(ssg_data_filename, 'w')
        for forecast in ssg_forecasts:
            data_file.write('%0.6f %0.6f\n' % (forecast.time,
                                               forecast.forecast))
        data_file.close()
    if not gap_forecasts and not ssg_forecasts:
        print('No forecasts made, so not creating forecast image.')
        return
    # Set terminal for the output files.
    forecast_script = 'set terminal png notransparent size 480,360\n\n'
    # Make settings for the plot.
    if self._filename is None:
        forecast_script += 'set title "Forecasts"\n'
    else:
        forecast_script += ('set title "Forecasts: %s, %s"\n' % (
                self._filename, self._label))
    forecast_script += 'set xlabel \"time (s)\"\n'
    forecast_script += 'set ylabel \"prediction of total time\"\n'
    forecast_script += 'set autoscale\n'
    # Plot the data points and the unit-slope line (to show elapsed time).
    forecast_script += 'plot '
    if forecast_forecasts:
        forecast_script += ('\'%s\' with linespoints linetype 3 '
                          'title \"(SSG)\", ' % ssg_data_filename)
    if gap_forecasts:
        forecast_script += ('\'%s\' with linespoints linetype 4 pointtype 19 '
        'title \"(MIP gap)\", ' % gap_data_filename)
    forecast_script += 'x linetype 0 title \"elapsed time\"\n'
    forecast_script += 'show output\n'

    return forecast_script
def GenerateHistogram(self, output_file=False)

Generate files necessary for a histogram image. Two files are necessary: a data file and a Gnuplot script file (which references the data file).

Args

time
Float number of seconds since the start of optimization.
Expand source code
def GenerateHistogram(self, output_file = False):
    """
    Generate files necessary for a histogram image.
    Two files are necessary: a data file and a Gnuplot script file (which
    references the data file).
    Args:
      time: Float number of seconds since the start of optimization.
    """
    num_bins = 20
    # Compute the bin width and counts.
    objective_list = []
    for n in self.get_node_list():
        if (self.get_node_attr(n,'status') == 'candidate' or
            self.get_node_attr(n,'status') == 'pregnant'):
            lp_bound = self.get_node_attr(n,'lp_bound')
            if not self.IsBetterThanIncumbent(lp_bound):
                continue
            objective_list.append(lp_bound)
    # TODO(aykut) added the following check, we need it since we generate
    # histograms real time
    # we can not generate histogram if we do not have upperl and lower
    #bounds
    if len(objective_list)==0 or self._incumbent_value is None:
        return None
    # The first time we create a histogram, set bounds for objective
    # values.
    # TODO(bhunsaker): Consider bounds; talk to Osman.
    if self._histogram_lower_bound is None:
        if self._optimization_sense == 'min':
            self._histogram_lower_bound = min(objective_list)
            if self._incumbent_value is not None:
                self._histogram_upper_bound = self._incumbent_value
            else:
                self._histogram_upper_bound = max(objective_list)
        else:
            self._histogram_upper_bound = max(objective_list)
            if self._incumbent_value is not None:
                self._histogram_lower_bound = self._incumbent_value
            else:
                self._histogram_lower_bound = min(objective_list)
    bin_width = old_div((self._histogram_upper_bound -
                 self._histogram_lower_bound), float(num_bins))
    bin_counts = [0.0 for i in range(num_bins)]
    for value in objective_list:
        bin = int(math.floor(old_div((value - self._histogram_lower_bound),
                             bin_width)))
        # Special case for the largest value.
        if (value >= self._histogram_upper_bound and
            value < self._histogram_upper_bound + 1e-6):
            bin = num_bins - 1
        if bin < 0:
            return
        assert bin < num_bins, '%d (%f) !< %d (%f)' % (
            bin, value, num_bins, self._histogram_upper_bound)
        bin_counts[bin] += 1
    max_bin_count = max(bin_counts)
    bin_centers = [i + 1.0 for i in range(len(bin_counts))]
    bin_widths = [1.0 for i in range(len(bin_counts))]
    self.AdjustHistogramEndBins(objective_list, num_bins, bin_width,
                                bin_counts, bin_centers, bin_widths)
    if self._optimization_sense == 'min':
        lp_bound = min(objective_list)
    else:
        lp_bound = max(objective_list)
    # Output the bin data to a file.
    index_string = self.GetImageCounterString()
    data_filename = 'histogram%s.dat' % index_string
    data_file = open(data_filename, 'w')
    for index in range(len(bin_counts)):
        data_file.write('%f %f %f\n' % (bin_centers[index],
                                        bin_counts[index],
                                        bin_widths[index]))
    data_file.close()
    histogram_script = self.WriteHistogramScript(num_bins, bin_width,
                       max_bin_count, lp_bound, data_filename, output_file)
    # TODO(bhunsaker): Temporary hack
    #   This allows the bounds to be reset until an incumbent is found.
    if self._incumbent_value is None:
        self._histogram_lower_bound = None
        self._histogram_upper_bound = None
    return histogram_script
def GenerateIncumbentPath(self)

Generate files necessary for an incumbent scatterplot path image. Two files are necessary: a data file and a Gnuplot script file (which references the data file).

Expand source code
def GenerateIncumbentPath(self):
    """
    Generate files necessary for an incumbent scatterplot path image.
    Two files are necessary: a data file and a Gnuplot script file (which
    references the data file).
    """
    if self._incumbent_parent is None:
        return
    if self._scatterplot_lower_bound is None:
        return
    if self._scatterplot_upper_bound is None:
        return
    index_string = self.GetImageCounterString()
    # Output data points.
    data_filename = 'incumbentpath%s.dat' % index_string
    data_file = open(data_filename, 'w')
    # Write objective values and integer infeasibility sum information
    # for ancestor nodes.
    data_file.write('0 %0.6f\n' % self._incumbent_value)
    parent = self._incumbent_parent
    # TODO(bhunsaker): I think the following assumes a unique value for the
    #   parent of the root.
    while parent != None:
        data_file.write('%0.6f %0.6f\n'
                        % (self.get_node_attr(parent,
                           'integer_infeasibility_sum'),
                         self.get_node_attr(parent, 'lp_bound')))
        parent = self.get_node_attr(parent, 'parent')
    data_file.close()
    self._incumbent_path_datafiles.append(data_filename)
    # Output the Gnuplot script to a file.
    path_script = self.WriteIncumbentPathScript(data_filename)
    return path_script
def GeneratePredictionImages(self)
Expand source code
def GeneratePredictionImages(self):
    gap_measures = self._objective_gap_forecaster.GetAllMeasures()
    ssg_measures = self._sum_subtree_gaps_forecaster.GetAllMeasures()
    # Check that there are values to process.
    if len(gap_measures) == 0 or len(ssg_measures) == 0:
        print('WARNING: Not printing prediction images because at least'+\
            ' one measure set is empty.')
        print('  Gap measures: %d' % len(gap_measures))
        print('  SSG measures: %d' % len(ssg_measures))
        return
    # Gap measures
    gap_data_filename = 'gap_measures.dat'
    data_file = open(gap_data_filename, 'w')
    for measure in gap_measures:
        data_file.write('%0.6f %0.6f\n' % (measure.time, measure.value))
    data_file.close()
    # SSG measures
    ssg_data_filename = 'ssg_measures.dat'
    data_file = open(ssg_data_filename, 'w')
    # We need to scale the SSG measures so that it will make sense to
    # look at them on the same plot with gap measures.
    scale_factor=old_div(float(gap_measures[0].value),float(ssg_measures[0].value))
    for measure in ssg_measures:
        data_file.write('%0.6f %0.6f\n' % (measure.time,
                                           measure.value * scale_factor))
    data_file.close()
    # Set terminal for the output files.
    measures_script = 'set terminal png notransparent size 480,360\n\n'
    # Make settings for the plot.
    if self.filename is None:
        measures_script += 'set title "Progress Measures"\n'
    else:
        measures_script += ('set title "Progress Measures: %s, %s"\n' % (
                self._filename, self._label))
    measures_script += 'set xlabel \"time (s)\"\n'
    measures_script += 'set ylabel \"measure\"\n'
    measures_script += 'set autoscale\n'
    # Plot the data points.
    measures_script += (
        'plot \'%s\' with linespoints linetype 3 title \"(SSG)\", '
        '\'%s\' with linespoints linetype 4 pointtype 19 '
        'title \"(MIP gap)\"\n' %
        (ssg_data_filename, gap_data_filename))
    measures_script += 'show output\n'

    return measures_script
def GenerateScatterplot(self, output_file=False)

Generate files necessary for a scatterplot image. Two files are necessary: a data file and a Gnuplot script file (which references the data file).

Args

output_file
if not given the gnuplot image will not be written

to disk but returned (to be displayed in matplotlib window)

Expand source code
def GenerateScatterplot(self, output_file = False):
    """
    Generate files necessary for a scatterplot image.
    Two files are necessary: a data file and a Gnuplot script file (which
    references the data file).
    Args:
        output_file: if not given the gnuplot image will not be written
    to disk but returned (to be displayed in matplotlib window)
    """
    # Output data points.
    index_string = self.GetImageCounterString()
    data_filename = 'scatterplot%s.dat' % index_string
    data_file = open(data_filename, 'w')
    if self._scatterplot_lower_bound is None:
        bounds = []
    # Write objective values and integer infeasibility sum information
    # for candidate and pregnant nodes.
    for node in self.get_node_list():
        status = self.get_node_attr(node, 'status')
        lp_bound = self.get_node_attr(node,'lp_bound')
        if status == 'candidate' or status == 'pregnant':
            # Optional check for fathomed nodes.
            if (self._fathom and
                not self.IsBetterThanIncumbent(lp_bound)):
                continue
            data_file.write('%0.6f %0.6f\n' % (
                    self.get_node_attr(node, 'integer_infeasibility_sum'),
                    lp_bound))
            # Set the image objective bounds the first image.
            if self._scatterplot_lower_bound is None:
                bounds.append(lp_bound)
    data_file.close()
    if self._scatterplot_lower_bound is None:
        if len(bounds) <= 1:
            return None
        self._scatterplot_lower_bound = min(bounds)
        self._scatterplot_upper_bound = max(bounds)
        # The incumbent overrides a bound if present.
        if self._incumbent_value is not None:
            if self._optimization_sense == 'min':
                self._scatterplot_upper_bound = self._incumbent_value
            else:
                self._scatterplot_lower_bound = self._incumbent_value
    scatterplot_script = self.WriteScatterplotScript(data_filename,
                                                     output_file)
    return scatterplot_script
def GenerateTreeImage(self, fixed_horizontal_positions=False)

Generate files necessary for a tree image. Two files are necessary: a data file and a Gnuplot script file (which references the data file).

Expand source code
def GenerateTreeImage(self, fixed_horizontal_positions = False):
    """
    Generate files necessary for a tree image.
    Two files are necessary: a data file and a Gnuplot script file (which
    references the data file).
    """
    index_string = self.GetImageCounterString()
    if fixed_horizontal_positions:
        name_prefix = 'tree_alt'
        horizontal_positions = self.GetTreeFixedHorizontalPositions()
    else:
        name_prefix = 'tree'
        horizontal_positions = self.GetTreeHorizontalPositions()
    candidate_lines = []
    pregnant_lines = []
    branched_lines = []
    infeasible_lines = []
    fathomed_lines = []
    integer_lines = []
    additional_script_lines = []
    node_list = self.get_node_list()
    print_edges = (len(node_list) <= self._edge_limit)
    for node in node_list:
        node_lp_bound = self.get_node_attr(node, 'lp_bound')
        if self.get_node_attr(node, 'status') == 'candidate':
            # TODO(bhunsaker): add optional fathoming check
            candidate_lines.append('%0.6f %0.6f\n' % (
                    horizontal_positions[node], node_lp_bound))
        elif self.get_node_attr(node, 'status') == 'pregnant':
            # TODO(bhunsaker): add optional fathoming check
            pregnant_lines.append('%0.6f %0.6f\n' % (
                    horizontal_positions[node], node_lp_bound))
        elif self.get_node_attr(node, 'status') == 'branched':
            branched_lines.append('%0.6f %0.6f\n' % (
                    horizontal_positions[node], node_lp_bound))
        elif self.get_node_attr(node, 'status') == 'infeasible':
            infeasible_lines.append('%0.6f %0.6f\n' % (
                    horizontal_positions[node], node_lp_bound))
        elif self.get_node_attr(node, 'status') == 'fathomed':
            fathomed_lines.append('%0.6f %0.6f\n' % (
                    horizontal_positions[node], node_lp_bound))
        elif self.get_node_attr(node, 'status') == 'integer':
            integer_lines.append('%0.6f %0.6f\n' % (
                    horizontal_positions[node], node_lp_bound))
        if print_edges and node != self.root.name:
            if True:
                _parent_id = self.get_node_attr(node, 'parent')
                additional_script_lines.append(
                 'set arrow from %0.6f, %0.6f to %0.6f, %0.6f nohead lt -1 '
                 'lw 0.2\n' % (horizontal_positions[_parent_id],
                 self.get_node_attr(_parent_id, 'lp_bound'),
                 horizontal_positions[node],
                 self.get_node_attr(node, 'lp_bound')))
    plot_parts = []
    # Plot root node.
    plot_parts.append('"< echo %0.6f %0.6f" w p lt 2 pt 7' %
                      (horizontal_positions[self.root.name],
                       self.root.get_attr('lp_bound')))
    # If desired, sample from the set of nodes rather than plotting all.
    if self._sample_tree:
        sample_size = self._sample_tree
        if len(branched_lines) > sample_size:
            branched_lines = random.sample(branched_lines, sample_size)
        if len(fathomed_lines) > sample_size:
            fathomed_lines = random.sample(fathomed_lines, sample_size)
        if len(infeasible_lines) > sample_size:
            infeasible_lines = random.sample(infeasible_lines, sample_size)
        if len(pregnant_lines) > sample_size:
            pregnant_lines = random.sample(pregnant_lines, sample_size)
        if len(candidate_lines) > sample_size:
            candidate_lines = random.sample(candidate_lines, sample_size)
        if len(integer_lines) > sample_size:
            integer_lines = random.sample(integer_lines, sample_size)
    # Output all data files.  Note that the order below matters.
    if len(branched_lines):
        self.WriteDataFileFromList('%s_branched%s.dat' % (name_prefix,
                                                          index_string),
                                   branched_lines)
        plot_parts.append('\'%s_branched%s.dat\' w p lt rgb "yellow" pt 7' %
                          (name_prefix, index_string))
    if len(fathomed_lines):
        self.WriteDataFileFromList('%s_fathomed%s.dat' % (name_prefix,
                                                          index_string),
                                   fathomed_lines)
        plot_parts.append('\'%s_fathomed%s.dat\' w p lt rgb "light-red" pt 7' %
                          (name_prefix, index_string))
    if len(infeasible_lines):
        self.WriteDataFileFromList('%s_infeasible%s.dat' % (name_prefix,
                                                            index_string),
                                   infeasible_lines)
        plot_parts.append('\'%s_infeasible%s.dat\' w p lt rgb "dark-red" pt 7' %
                          (name_prefix, index_string))
    if len(pregnant_lines):
        self.WriteDataFileFromList('%s_pregnant%s.dat' % (name_prefix,
                                                          index_string),
                                   pregnant_lines)
        plot_parts.append('\'%s_pregnant%s.dat\' w p lt rgb "green" pt 7' %
                          (name_prefix, index_string))
    if len(candidate_lines):
        for line in candidate_lines:
            plot_parts.append('"< echo %s" w p lt rgb "green" pt 7'
                              %line.rstrip('\r\n'))
    if len(integer_lines):
        self.WriteDataFileFromList('%s_integer%s.dat' % (name_prefix,
                                                         index_string),
                                   integer_lines)
        plot_parts.append('\'%s_integer%s.dat\' w p lt rgb "cyan" pt 7' %
                          (name_prefix, index_string))
    if self._incumbent_value is not None:
        plot_parts.append('%0.6f lt 1 lw 0.5' % self._incumbent_value)
    additional_script_lines.append('plot %s\n' % ', '.join(plot_parts))
    additional_script_lines.append('unset arrow\n')
    image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
        self._min_objective_value, self._max_objective_value)
    data = ''
    data += 'set terminal png notransparent size 480,360\n'
    data += 'set nokey\n'
    data += 'set autoscale\n'
    data += 'set tics scale 0.001\n'
    data += 'set pointsize 0.5\n'
    data += 'set xrange [-0.1:1.1]\n'
    data += 'set yrange [%0.6f:%0.6f]\n' % (image_max_obj,
                                                      image_min_obj)
    data += 'set format x ""\n'
    data += 'set ylabel "obj. value"\n'
    if self._filename is None:
        data += 'set title "B&B tree"\n\n'
    else:
        data += 'set title "B&B tree (%s %.2fs %s)"\n\n' % (
            self._filename, self._time, self._label)
    for line in additional_script_lines:
        data += line

    return data
def GetImageCounterString(self)

Returns a string with the image counter.

Expand source code
def GetImageCounterString(self):
    """
    Returns a string with the image counter.
    """
    return '%03d' % self._image_counter
def GetImageObjectiveBounds(self, min_value, max_value)

Return min and max bounds to be used for images. Images should use bounds that are slightly wider than observed objective values. Also, the special case of a single value must be handled.

Args

min_value
Float minimum objective value.
max_value
Float maximum objective value.

Returns

A tuple of two float values (lower_bound, upper_bound).

Expand source code
def GetImageObjectiveBounds(self, min_value, max_value):
    """
    Return min and max bounds to be used for images.
    Images should use bounds that are slightly wider than observed
    objective values.  Also, the special case of a single value must be
    handled.
    Args:
      min_value: Float minimum objective value.
      max_value: Float maximum objective value.
    Returns:
      A tuple of two float values (lower_bound, upper_bound).
    """
    obj_range = max_value - min_value
    if obj_range > 0:
        image_max_obj = max_value + 0.01 * obj_range
        image_min_obj = min_value - 0.01 * obj_range
    else:
        if max_value >= 0:
            image_max_obj = 1.01 * max_value
        else:
            image_max_obj = 0.99 * max_value
        if min_value >= 0:
            image_min_obj = 0.99 * min_value
        else:
            image_min_obj = 1.01 * min_value
    return (image_min_obj, image_max_obj)
def GetTreeFixedHorizontalPositions(self)

Returns horizontal positions for all nodes based on fixed positions.

Returns

Dictionary of float horizontal positions, keyed by node id.

Expand source code
def GetTreeFixedHorizontalPositions(self):
    """
    Returns horizontal positions for all nodes based on fixed positions.
    Returns:
      Dictionary of float horizontal positions, keyed by node id.
    """
    # Statistics needed for horizontal positions.
    horizontal_lower_bound = dict.fromkeys(self.get_node_list(), 0.0)
    horizontal_upper_bound = dict.fromkeys(self.get_node_list(), 1.0)
    horizontal_positions = dict.fromkeys(self.get_node_list())
    horizontal_positions[self.root.name] = 0.5
    # sort node list
    node_id_list = sorted(self.get_node_list())
    node_id_list_int = list(int(n) for n in node_id_list)
    node_id_list_int = sorted(node_id_list_int)
    node_id_list = list(str(n) for n in node_id_list_int)
    for node_id in node_id_list:
        if node_id == self.root.name:
            continue
        parent_id = self.get_node_attr(node_id, 'parent')
        branch_direction = self.get_node_attr(node_id, 'direction')
        if branch_direction == 'R':
            horizontal_lower_bound[node_id] = horizontal_positions[
                parent_id]
            horizontal_upper_bound[node_id] = horizontal_upper_bound[
                parent_id]
        elif branch_direction == 'L':
            horizontal_lower_bound[node_id] = horizontal_lower_bound[
                parent_id]
            horizontal_upper_bound[node_id] = horizontal_positions[
                parent_id]
        else:
            print('Error: node %s has unsupported branching direction.'\
                %node_id)
            print('Fixed-position tree images only support L and R ')
            print('branching.')
            sys.exit(1)
        horizontal_positions[node_id] = old_div((
            horizontal_upper_bound[node_id] +
            horizontal_lower_bound[node_id]), 2)
    return horizontal_positions
def GetTreeHorizontalPositions(self)

Returns horizontal positions for all nodes. Each node is given equal horizontal space.

Returns

Dictionary of float horizontal positions, keyed by node id.

Expand source code
def GetTreeHorizontalPositions(self):
    """
    Returns horizontal positions for all nodes.
    Each node is given equal horizontal space.
    Returns:
      Dictionary of float horizontal positions, keyed by node id.
    """
    # Statistics needed for horizontal positions.
    number_descendants = dict.fromkeys(self.get_node_list(), 1)
    # number_descendants includes the key node itself
    horizontal_lower_bound = dict.fromkeys(self.get_node_list(), 0.0)
    horizontal_upper_bound = dict.fromkeys(self.get_node_list(), 1.0)
    horizontal_positions = dict.fromkeys(self.get_node_list())
    visited = dict.fromkeys(self.get_node_list(), False)
    # Count the number of descendants for each node.
    # Do a post-order traversal of the tree.
    node_stack = []
    node_stack.append(self.root.name)
    while node_stack:
        current_node = node_stack[len(node_stack) - 1]
        lchild = self.get_left_child(current_node)
        rchild = self.get_right_child(current_node)
        is_node_added = False
        # Add the next unvisited child to the stack
        if lchild is not None and not visited[quote(lchild)]:
            node_stack.append(lchild)
            is_node_added = True
        if (rchild is not None and not visited[quote(rchild)] and
            is_node_added==False):
            node_stack.append(rchild)
            is_node_added = True
        # If all childs visited, then update number_descendants
        if not is_node_added:
            if lchild is not None:
                number_descendants[quote(current_node)] += (
                            number_descendants[quote(lchild)])
            if rchild is not None:
                number_descendants[quote(current_node)] += (
                            number_descendants[quote(rchild)])
            visited[quote(current_node)] = True
            del node_stack[len(node_stack) - 1]
    # Traverse the tree and set horizontal positions.
    # Do a pre-order traversal of the tree.
    node_stack = []
    node_stack.append(self.root.name)
    horizontal_lower_bound[self.root.name] = 0.0
    horizontal_upper_bound[self.root.name] = 1.0
    while node_stack:
        node = node_stack.pop()
        lchild = self.get_left_child(node)
        rchild = self.get_right_child(node)
        direction = None
        number_of_children = 0
        children_list = []
        # Place all children on the stack
        if lchild is not None:
            node_stack.append(lchild)
            number_of_children += 1
            direction = 'L'
            children_list.append(lchild)
        if rchild is not None:
            node_stack.append(rchild)
            number_of_children += 1
            direction = 'R'
            children_list.append(rchild)
        # Convenience variables
        current_lower_bound = horizontal_lower_bound[quote(node)]
        current_range = (horizontal_upper_bound[quote(node)] -
                         horizontal_lower_bound[quote(node)])
        total_descendants = number_descendants[quote(node)]
        sorted_child_labels = sorted(children_list)
        # Determine where to place this node with respect to its children.
        # Put the node in the center, or have more children on the left.
        before_index = int(math.ceil(old_div(number_of_children,2.0)))
        # Exception with a single node that is 'L'
        if number_of_children == 1:
            if direction != 'L':
                before_index = 0
        cumulative_descendants = 0
        for i, label in enumerate(sorted_child_labels):
            if before_index == i:
                # Determine the relative position for the current node
                relative_position = old_div((cumulative_descendants + 0.5), (
                    total_descendants))
                cumulative_descendants += 1
            # Set bounds for this child
            horizontal_lower_bound[quote(label)] = (
                current_lower_bound + float(cumulative_descendants) /
                total_descendants * current_range)
            # Increment cumulative_descendants, which also lets us compute
            # the upper bound.
            cumulative_descendants += number_descendants[quote(label)]
            horizontal_upper_bound[quote(label)] = (
                current_lower_bound + float(cumulative_descendants) /
                total_descendants * current_range)
        # Catch the case that the node comes after all its children.
        # This must also work for the case that this is the only node.
        if before_index == len(sorted_child_labels):
            relative_position = old_div((cumulative_descendants + 0.5), (
                total_descendants))
        # Finally set the position for the current node
        horizontal_positions[quote(node)] = (
            horizontal_lower_bound[quote(node)] + relative_position * (
                horizontal_upper_bound[quote(node)] -
                horizontal_lower_bound[quote(node)]))
    return horizontal_positions
def IsBetterThan(self, value1, value2)

Returns True if value1 is better than value2 as an objective value. This depends on the optimization sense of the instance.

Args

value1
Float.
value2
Float.

Returns

True if value1 is better than value2 as an objective value.

Expand source code
def IsBetterThan(self, value1, value2):
    """
    Returns True if value1 is better than value2 as an objective value.
    This depends on the optimization sense of the instance.
    Args:
      value1: Float.
      value2: Float.
    Returns:
      True if value1 is better than value2 as an objective value.
    """
    if self._optimization_sense is None:
        print("Optimization sense is not set, assuming sense is miniminzation")
        self._optimization_sense = 'min'
    if self._optimization_sense == 'min':
        return value1 < value2
    else:
        return value1 > value2
def IsBetterThanIncumbent(self, value)

Returns True if the passed value is better than current incumbent.

Args

value
Float to use for comparison.

Returns

True if the passed value is better than the current incumbent. 'Better' is determined by the sense of optimization.

Expand source code
def IsBetterThanIncumbent(self, value):
    """
    Returns True if the passed value is better than current incumbent.
    Args:
      value: Float to use for comparison.
    Returns:
      True if the passed value is better than the current incumbent.
      'Better' is determined by the sense of optimization.
    """
    if self._incumbent_value is None:
        return True
    else:
        return self.IsBetterThan(value, self._incumbent_value)
def ProcessBranchedLine(self, node_id, parent_id, branch_direction, remaining_tokens)

Core processing for a line of type 'branched'.

Args

node_id
String node id.
parent_id
String node id of parent.
branch_direction
String of 'L' or 'R' indicating whether this node
is the left or right child of its parent.
remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessBranchedLine(self, node_id, parent_id, branch_direction,
                        remaining_tokens):
    """
    Core processing for a line of type 'branched'.
    Args:
      node_id: String node id.
      parent_id: String node id of parent.
      branch_direction: String of 'L' or 'R' indicating whether this node
      is the left or right child of its parent.
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Parse remaining tokens
    if len(remaining_tokens) not in [3, 5]:
        print('Invalid line: %s branched %s %s %s %s' % (
                self._time, node_id, parent_id, branch_direction,
                ' '.join(remaining_tokens)))
        print('Should match: <time> branched <node id> <parent id> ')
        print('<branch direction> <lp bound> ')
        print('<sum of integer infeasibilities> <number of integer ')
        print('infeasibilities>')
        sys.exit(1)
    lp_bound = float(remaining_tokens[0])
    integer_infeasibility_sum = float(remaining_tokens[1])
    integer_infeasibility_count = int(remaining_tokens[2])
    condition_begin = None
    condition_end = None
    if len(remaining_tokens) == 5:
        # In this case, we must also be printing conditions numbers
        condition_begin = int(remaining_tokens[3])
        condition_end = int(remaining_tokens[4])
    self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'branched',
                         lp_bound, integer_infeasibility_count,
                         integer_infeasibility_sum, condition_begin,
                         condition_end)
def ProcessCandidateLine(self, node_id, parent_id, branch_direction, remaining_tokens)

Core processing for a line of type 'candidate'.

Args

node_id
String node id.
parent_id
String node id of parent.
branch_direction
String of 'L' or 'R' indicating whether this node
is the left or right child of its parent.
remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessCandidateLine(self, node_id, parent_id, branch_direction,
                         remaining_tokens):
    """
    Core processing for a line of type 'candidate'.
    Args:
      node_id: String node id.
      parent_id: String node id of parent.
      branch_direction: String of 'L' or 'R' indicating whether this node
      is the left or right child of its parent.
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Parse remaining tokens
    if len(remaining_tokens) == 2 or len(remaining_tokens) > 3:
        print('Invalid line: %s branched %s %s %s %s' % (
                self._time, node_id, parent_id, branch_direction,
                ' '.join(remaining_tokens)))
        print('Should match: <time> candidate <node id> <parent id> ')
        print('<branch direction> [<lp bound>] ')
        print('[<sum of integer infeasibilities> <number of integer ')
        print('infeasibilities>]')
        sys.exit(1)
    # if parent_id not in self.get_node_list():
    #     print 'Error: node %s not in set' % parent_id
    #     sys.exit(1)
    # TODO(bhunsaker): Check that we handle the cases of updating a
    #candidate.
    if len(remaining_tokens) > 0:
        lp_bound = float(remaining_tokens[0])
    else:
        lp_bound = self.get_node_attr(parent_id, 'lp_bound')
    if len(remaining_tokens) == 3:
        integer_infeasibility_sum = float(remaining_tokens[1])
        integer_infeasibility_count = int(remaining_tokens[2])
    else:
        integer_infeasibility_sum = self.get_node_attr(parent_id,
                                              'integer_infeasibility_sum')
        integer_infeasibility_count = self.get_node_attr(parent_id,
                                            'integer_infeasibility_count')
    self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'candidate',
                         lp_bound, integer_infeasibility_count,
                         integer_infeasibility_sum)
def ProcessFathomedLine(self, node_id, parent_id, branch_direction, remaining_tokens)

Core processing for a line of type 'fathomed'.

Args

node_id
String node id.
parent_id
String node id of parent.
branch_direction
String of 'L' or 'R' indicating whether this node is the left or right child of its parent.
remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessFathomedLine(self, node_id, parent_id, branch_direction,
                        remaining_tokens):
    """
    Core processing for a line of type 'fathomed'.
    Args:
      node_id: String node id.
      parent_id: String node id of parent.
      branch_direction: String of 'L' or 'R' indicating whether this node is
        the left or right child of its parent.
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Print a warning if there is no current incumbent.
    if self._incumbent_value is None:
        print('WARNING: Encountered "fathom" line before first incumbent.')
        print('  This may indicate an error in the input file.')
    # Parse remaining tokens
    if len(remaining_tokens) > 1:
        print('Invalid line: %s fathomed %s %s %s %s' % (
                self._time, node_id, parent_id, branch_direction,
                ' '.join(remaining_tokens)))
        print('Should match: <time> fathomed <node id> <parent id>'+\
            '<branch direction> [<lp bound>]')
        sys.exit(1)
    if len(remaining_tokens) == 1:
        lp_bound = float(remaining_tokens[0])
    else:
        if (node_id in self.get_node_list() and
            self.get_node_attr(node_id, 'lp_bound') is not None):
            lp_bound = self.get_node_attr(node_id, 'lp_bound')
        else:
            lp_bound = self.get_node_attr(parent_id, 'lp_bound')
        if self._optimization_sense == 'min':
            if (self._incumbent_value is not None and
                lp_bound < self._incumbent_value):
                lp_bound = self._incumbent_value
        elif self._optimization_sense == 'max':
            if (self._incumbent_value is not None and
                lp_bound > self._incumbent_value):
                lp_bound = self._incumbent_value
    parent_node = self.get_node(parent_id)
    self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'fathomed',
                         lp_bound,
                         self.get_node_attr(parent_id,
                                            'integer_infeasibility_count'),
                         self.get_node_attr(parent_id,
                                            'integer_infeasibility_sum'))
def ProcessHeuristicLine(self, remaining_tokens)

Core processing for a line of type 'heuristic'.

Args

remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessHeuristicLine(self, remaining_tokens):
    """
    Core processing for a line of type 'heuristic'.
    Args:
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Parse remaining tokens
    if len(remaining_tokens) < 1 or len(remaining_tokens) > 2:
        print('Invalid line: %s heuristic %s' % (
                self._time, ' '.join(remaining_tokens)))
        print('Should match: <time> heuristic <obj value>'+\
            ' [<associated node id>]')
        sys.exit(1)
    objective_value = float(remaining_tokens[0])
    if len(remaining_tokens) == 2:
        associated_node = remaining_tokens[1]
    else:
        associated_node = None
    # Check that this is actually an improvement
    if self._incumbent_value is not None and self._optimization_sense is None:
        if objective_value > self._incumbent_value:
            print("Objective sense unset, guessing maximization")
            self._optimization_sense = 'max'
        else:
            print("Objective sense unset, guessing minimization")
            self._optimization_sense = 'min'
    if not self.IsBetterThanIncumbent(objective_value):
        return
    self._previous_incumbent_value = self._incumbent_value
    self._incumbent_value = objective_value
    self.UpdateObjectiveValueLimits(objective_value)
    self._incumbent_parent = associated_node
    # Set variable to generate images
    self._new_integer_solution = True
def ProcessInfeasibleLine(self, node_id, parent_id, branch_direction, remaining_tokens)

Core processing for a line of type 'infeasible'.

Args

node_id
String node id.
parent_id
String node id of parent.
branch_direction
String of 'L' or 'R' indicating whether this node is the left or right child of its parent.
remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessInfeasibleLine(self, node_id, parent_id, branch_direction,
                          remaining_tokens):
    """
    Core processing for a line of type 'infeasible'.
    Args:
      node_id: String node id.
      parent_id: String node id of parent.
      branch_direction: String of 'L' or 'R' indicating whether this node is
        the left or right child of its parent.
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Parse remaining tokens
    if len(remaining_tokens) not in [0, 2]:
        print('Invalid line: %s infeasible %s %s %s %s' % (
                self._time, node_id, parent_id, branch_direction,
                ' '.join(remaining_tokens)))
        print('Should match: <time> infeasible <node id> <parent id> ')
        print('<branch direction>')
        sys.exit(1)
    # Use parent values if the node does not have its own
    lp_bound = self.get_node_attr(parent_id, 'lp_bound')
    ii_count = self.get_node_attr(parent_id, 'integer_infeasibility_count')
    ii_sum = self.get_node_attr(parent_id, 'integer_infeasibility_sum')
    if node_id in self.get_node_list():
        if self.get_node_attr(node_id, 'lp_bound') is not None:
            lp_bound = self.get_node_attr(node_id, 'lp_bound')
        if (self.get_node_attr(node_id, 'integer_infeasibility_count')
            is not None):
            ii_count = self.get_node_attr(node_id,
                                          'integer_infeasibility_count')
        if (self.get_node_attr(node_id, 'integer_infeasibility_sum')
            is not None):
            ii_sum = self.get_node_attr(node_id,'integer_infeasibility_sum')
    if len(remaining_tokens) == 2:
        # In this case, we must also be printing conditions numbers
        condition_begin = int(remaining_tokens[0])
        condition_end = int(remaining_tokens[1])
    self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'infeasible',
                         lp_bound, ii_count, ii_sum)
def ProcessIntegerLine(self, node_id, parent_id, branch_direction, remaining_tokens)

Core processing for a line of type 'integer'.

Args

node_id
String node id.
parent_id
String node id of parent.
branch_direction
String of 'L' or 'R' indicating whether this node
is the left or right child of its parent.
remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessIntegerLine(self, node_id, parent_id, branch_direction,
                       remaining_tokens):
    """
    Core processing for a line of type 'integer'.
    Args:
      node_id: String node id.
      parent_id: String node id of parent.
      branch_direction: String of 'L' or 'R' indicating whether this node
      is the left or right child of its parent.
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Parse remaining tokens
    if len(remaining_tokens) != 1:
        print('Invalid line: %s integer %s %s %s %s' % (
                self._time, node_id, parent_id, branch_direction,
                ' '.join(remaining_tokens)))
        print('Should match: <time> integer <node id> <parent id>'+\
            '<branch direction> <obj value>')
        sys.exit(1)
    objective_value = float(remaining_tokens[0])
    self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'integer',
                         objective_value, None, None)
    self._previous_incumbent_value = self._incumbent_value
    self._incumbent_value = objective_value
    self._incumbent_parent = parent_id
    self._new_integer_solution = True
def ProcessLine(self, line)

Process a line of the input file, generating images if appropriate. Parses the line, updates internal data structures, and creates images if appropriate.

Args

line
String input line to process.
Expand source code
def ProcessLine(self, line):
    """
    Process a line of the input file, generating images if appropriate.
    Parses the line, updates internal data structures, and creates images
    if appropriate.
    Args:
      line: String input line to process.
    """
    line = line.strip()
    # Comments start with a '#'
    if line[0] == '#':
        return
    tokens = line.split()
    if len(tokens) < 3:
        print('Incomplete or invalid line: %s' %' '.join(tokens))
        sys.exit(1)
    # Tokens shared by all line types
    self._time = float(tokens[0])
    line_type = tokens[1]
    remaining_tokens = tokens[2:]
    # Process the line based on the type
    if line_type == 'heuristic':
        self._optimal_soln_time = self._time
        self.ProcessHeuristicLine(remaining_tokens)
    else:
        # Other node types share common tokens
        node_id = int(tokens[2])
        parent_id = int(tokens[3])
        branch_direction = tokens[4]
        remaining_tokens = tokens[5:]
        # TODO(aykut):parent id of root node is 0 when we read from file.
        if id==self.root:
            parent_id = None
        # Check that the parent node id is valid
        # elif parent_id not in self.get_node_list() and self.root is not None:
        #     print 'Parent id does not exist: %s' % line
        #     sys.exit(1)
        if line_type == 'integer':
            self._optimal_soln_time = self._time
            self.ProcessIntegerLine(node_id, parent_id,
                                    branch_direction, remaining_tokens)
        elif line_type == 'fathomed':
            self.ProcessFathomedLine(node_id, parent_id,
                                     branch_direction, remaining_tokens)
        elif line_type == 'candidate':
            self.ProcessCandidateLine(node_id, parent_id,
                                      branch_direction, remaining_tokens)
        elif line_type == 'pregnant':
            self.ProcessPregnantLine(node_id, parent_id,
                                     branch_direction, remaining_tokens)
        elif line_type == 'branched':
            self.ProcessBranchedLine(node_id, parent_id,
                                     branch_direction, remaining_tokens)
        elif line_type == 'infeasible':
            self.ProcessInfeasibleLine(node_id, parent_id,
                                       branch_direction, remaining_tokens)
        else:
            print('Unexpected line type "%s": %s' % (line_type,
                                                     ' '.join(tokens)))
            sys.exit(1)
def ProcessPregnantLine(self, node_id, parent_id, branch_direction, remaining_tokens)

Core processing for a line of type 'pregnant'.

Args

node_id
String node id.
parent_id
String node id of parent.
branch_direction
String of 'L' or 'R' indicating whether this node is the left or right child of its parent.
remaining_tokens
List of string tokens. These are those that remain after any common tokens are processed.
Expand source code
def ProcessPregnantLine(self, node_id, parent_id, branch_direction,
                        remaining_tokens):
    """
    Core processing for a line of type 'pregnant'.
    Args:
      node_id: String node id.
      parent_id: String node id of parent.
      branch_direction: String of 'L' or 'R' indicating whether this node is
        the left or right child of its parent.
      remaining_tokens: List of string tokens. These are those that remain
        after any common tokens are processed.
    """
    # Parse remaining tokens
    if len(remaining_tokens) != 3:
        print('Invalid line: %s pregnant %s %s %s %s' % (
                self._time, node_id, parent_id, branch_direction,
                ' '.join(remaining_tokens)))
        print('Should match: <time> pregnant <node id> <parent id> ')
        print('<branch direction> <lp bound> ')
        print('<sum of integer infeasibilities> <number of integer ')
        print('infeasibilities>')
        sys.exit(1)
    lp_bound = float(remaining_tokens[0])
    integer_infeasibility_sum = float(remaining_tokens[1])
    integer_infeasibility_count = int(remaining_tokens[2])

    self.AddOrUpdateNode(node_id, parent_id, branch_direction, 'pregnant',
                         lp_bound, integer_infeasibility_count,
                         integer_infeasibility_sum)
def RunGnuplotOnAllFiles(self)

Runs Gnuplot on all files in self._gnuplot_files.

Expand source code
def RunGnuplotOnAllFiles(self):
    """Runs Gnuplot on all files in self._gnuplot_files."""
    for file in self._gnuplot_files:
        subprocess.call(['gnuplot', file])
def UpdateObjectiveValueLimits(self, value)

Updates the min and max objective values if appropriate.

Args

value
Float objective value.
Expand source code
def UpdateObjectiveValueLimits(self, value):
    """Updates the min and max objective values if appropriate.
    Args:
      value: Float objective value.
    """
    if self._max_objective_value is None:
        self._max_objective_value = value
        self._min_objective_value = value
    else:
        if value > self._max_objective_value:
            self._max_objective_value = value
        if value < self._min_objective_value:
            self._min_objective_value = value
def WriteAllIncumbentPathsScript(self)

Return a Gnuplot script string to generate an incumbent path image.

Args

data_filenames
List of string names of files.
Expand source code
def WriteAllIncumbentPathsScript(self):
    """
    Return a Gnuplot script string to generate an incumbent path image.
    Args:
      data_filenames: List of string names of files.
    """
    data_filenames = self._incumbent_path_datafiles
    image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
        self._scatterplot_lower_bound, self._scatterplot_upper_bound)
    script = ''
    # Set terminal for the output files.
    script += 'set terminal png notransparent size 480,360\n\n'

    # Make settings for the scatter plot.
    if self._filename is None:
        script += 'set title "Incumbent paths"\n'
    else:
        script += ('set title "Incumbent paths (%s %.2fs %s)"\n' % (
                self._filename, self._time, self._label))
    script += 'set pointsize 0.8\n'
    script += 'set nokey\n'
    script += 'set xlabel \"sum of int. infeas.\"\n'
    script += 'set ylabel \"obj. value\"\n'
    script += ('set xrange [0:%0.6f+2]\n' %
                      self._max_integer_infeasibility_sum)
    script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                      image_max_obj))
    # Plot the data points and connecting lines.
    command_list = []
    for filename in data_filenames:
        command_list.append('\'%s\' with points pointtype 2, '
                            '\'%s\' with lines linetype 2' %
                            (filename, filename))
    script += 'plot %s\n' % ','.join(command_list)
    script += 'show output\n'
    return script
def WriteDataFileFromList(self, filename, data_list)

Write a list of string data to a file with one entry per line.

Args

filename
String filename to open.
data_list
List of string values to write.
Expand source code
def WriteDataFileFromList(self, filename, data_list):
    """
    Write a list of string data to a file with one entry per line.
    Args:
      filename: String filename to open.
      data_list: List of string values to write.
    """
    outfile = open(filename, 'w')
    for line in data_list:
        outfile.write(line)
    outfile.close()
def WriteHistogramScript(self, num_bins, bin_width, max_bin_count, lp_bound, data_filename, output_file)

Write a Gnuplot script file to generate a histogram image.

Args

num_bins
Integer number of bins for the histogram.
bin_width
Float width of the bins in terms of objective values.
max_bin_count
Integer number of the highest bin count.
lp_bound
Float value of the current LP bound.
data_filename
String name of the file; used for display purposes.
Expand source code
def WriteHistogramScript(self, num_bins, bin_width, max_bin_count,
                             lp_bound, data_filename, output_file):
    """
    Write a Gnuplot script file to generate a histogram image.
    Args:
      num_bins: Integer number of bins for the histogram.
      bin_width: Float width of the bins in terms of objective values.
      max_bin_count: Integer number of the highest bin count.
      lp_bound: Float value of the current LP bound.
      data_filename: String name of the file; used for display purposes.
    """
    # TODO(bhunsaker): add checks for bin_width zero
    if self._incumbent_value is not None:
        incumbent_bin = 1 + ((self._incumbent_value -
                              self._histogram_lower_bound) // bin_width)
        incumbent_x_coord = 0.5 + (old_div((self._incumbent_value -
                                    self._histogram_lower_bound),
                                   bin_width))
    lp_bound_bin = 1 + ((lp_bound - self._histogram_lower_bound) //
                        bin_width)
    lp_bound_x_coord = 0.5 + (old_div((lp_bound - self._histogram_lower_bound),
                              bin_width))
    # TODO(bhunsaker): Ask Osman about adjust_xcoord option, which appears
    #    to put the vertical lines at the edge of bins rather than the
    #    true location.
    # Output the Gnuplot script to a file.
    script = ""
    # Set terminal for the output files.
    script += 'set terminal png notransparent size 480,360\n\n'
    # Make settings for the scatter plot.
    index_string = self.GetImageCounterString()
    output_filename = "histogram."+index_string+".png"
    if output_file:
        script += 'set output "%s"\n' % output_filename
    if self._filename is None:
        script += 'set title "Histogram of LP Bounds"\n'
    else:
        script += ('set title "Histogram of LP Bounds: %s, %s, %.2fs"\n'
                   % (self._filename, self._label, self._time))
    script += 'set xlabel "obj. value"\n'
    script += 'set ylabel "number of nodes"\n'
    if self._logscaley:
        script += 'set logscale y\n'
    script += 'set nokey\n'
    script += 'set tics scale 0.001\n'

    script += 'set xrange [0:%d+1]\n' % num_bins
    if self._logscaley:
        script += 'set yrange [1:%d*1.2]\n' % max_bin_count
    else:
        script += 'set yrange [0:%d*1.2]\n' % max_bin_count
    script += 'set xtics rotate by 90\n'
    # Mark tics for each bin.
    script += 'set xtics ('
    # TODO(bhunsaker): Consider putting this in a loop.
    x_values = ['"%0.2f" %0.2f' %
                (self._histogram_lower_bound + i * bin_width, i + 0.5)
                for i in range(num_bins + 1)]
    script += ', '.join(x_values) + ')\n'
    # Plot LP bound and incumbent tics.
    script += 'set x2tics ('
    script += '"%0.2f" %d' % (lp_bound, lp_bound_bin)
    if self._incumbent_value is not None:
        script += ', "%0.2f"%d)\n' % (self._incumbent_value,
                                              incumbent_bin)
    else:
        script += ')\n'
    plot_parts = []
    # Plot the data points.
    plot_parts.append('\'%s\' with boxes fill solid 0.2' % data_filename)
    # Draw the vertical lp_bound and incumbent lines.
    script += 'set parametric\n'
    script += 'set trange [0:%d*1.5]\n' % max_bin_count
    plot_parts.append('%0.2f,t linetype 2' % lp_bound_x_coord)
    if self._incumbent_value is not None:
        plot_parts.append('%0.2f,t linetype 5' % incumbent_x_coord)
    script += 'plot %s\n' % ', '.join(plot_parts)
    script += 'unset parametric\n'
    script += 'show output\n'
    return script
def WriteIncumbentPathScript(self, data_filename)

Write a Gnuplot script file to generate an incumbent path image.

Args

data_filename
String name of the file; used for display purposes.
Expand source code
def WriteIncumbentPathScript(self, data_filename):
    """
    Write a Gnuplot script file to generate an incumbent path image.
    Args:
      data_filename: String name of the file; used for display purposes.
    """
    image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
        self._scatterplot_lower_bound, self._scatterplot_upper_bound)
    script = ''
    # Set terminal for the output files.
    script += 'set terminal png notransparent size 480,360\n\n'
    if self._filename is None:
        script += 'set title "Incumbent path"\n'
    else:
        script += ('set title "Incumbent path (%s %.2fs %s)"\n' % (
                self._filename, self._time, self._label))
    script += 'set pointsize 0.8\n'
    script += 'set nokey\n'
    script += 'set xlabel \"sum of int. infeas.\"\n'
    script += 'set ylabel \"obj. value\"\n'
    script += ('set xrange [0:%0.6f+2]\n' %
                      self._max_integer_infeasibility_sum)
    script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                      image_max_obj))
    # Plot the data points and connecting lines.
    script += ('plot \'%s\' with points pointtype 2, '
                      '\'%s\' with lines linetype 2\n' %
                      (data_filename, data_filename))
    script += 'show output\n'
    return script
def WriteScatterplotScript(self, data_filename, output_file)

Write a Gnuplot script file to generate a scatterplot image.

Args

data_filename
String name of the file; used for display purposes.
Expand source code
def WriteScatterplotScript(self, data_filename, output_file):
    """
    Write a Gnuplot script file to generate a scatterplot image.
    Args:
      data_filename: String name of the file; used for display purposes.
    """
    image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
        self._scatterplot_lower_bound, self._scatterplot_upper_bound)
    index_string = self.GetImageCounterString()
    output_filename = "scatterplot."+index_string+".png"
    script = ""
    # Set terminal for the output files.
    script += 'set terminal png notransparent size 480,360\n\n'
    # Make settings for the scatter plot.
    if output_file:
        script += 'set output "%s"\n' % output_filename
    if self._filename is None:
        script += 'set title "Scatterplot"\n'
    else:
        script += ('set title "Scatterplot: %s, %s, %ds"\n' % (
                self._filename, self._label, int(self._time)))
    script += 'set pointsize 0.8\n'
    script += 'set nokey\n'
    script += 'set xlabel \"sum of int. infeas.\"\n'
    script += 'set ylabel \"obj. value\"\n'
    script += ('set xrange [0:%0.6f+2]\n' %
                      self._max_integer_infeasibility_sum)
    script += ('set yrange [%0.6f:%0.6f]\n' % (image_min_obj,
                                                      image_max_obj))
    plot_parts = []
    # Plot the data points.
    plot_parts.append('\'%s\' with points pointtype 2 linetype 1' %
                      data_filename)
    # Also plot the incumbent line.
    if self._incumbent_value is not None:
        plot_parts.append('%0.6f linetype 2 linewidth 0.5' %
                          self._incumbent_value)
    # Plot the incumbent's parent if it's available.
    if self._incumbent_parent is not None:
        #incumbent_parent = self.get_node(self._incumbent_parent)
        plot_parts.append('"< echo %0.6f %0.6f" '
                          'with points pointtype 9 pointsize 1.2' %
                          (self.get_node_attr(self._incumbent_parent,
                                              'integer_infeasibility_sum'),
                           self.get_node_attr(self._incumbent_parent,
                                              'lp_bound')))
    script += 'plot %s\n' % ', '.join(plot_parts)
    script += 'show output\n'
    return script
def WriteTreeScript(self, additional_lines=None)

Write a Gnuplot script file to generate a tree image.

Args

additional_lines
String with additional lines to be added to the script file.
Expand source code
def WriteTreeScript(self, additional_lines = None):
    """
    Write a Gnuplot script file to generate a tree image.
    Args:
      additional_lines: String with additional lines to be added to the
        script file.
    """
    image_min_obj, image_max_obj = self.GetImageObjectiveBounds(
        self._min_objective_value, self._max_objective_value)
    data = ''
    data += 'set terminal png notransparent size 480,360\n'
    data += 'set output "%s"\n' % output_file
    data += 'set nokey\n'
    data += 'set autoscale\n'
    data += 'set tics scale 0.001\n'
    data += 'set pointsize 0.5\n'
    data += 'set xrange [-0.1:1.1]\n'
    data += 'set yrange [%0.6f:%0.6f]\n' % (image_max_obj,
                                                      image_min_obj)
    data += 'set format x ""\n'
    data += 'set ylabel "obj. value"\n'
    if self._filename is None:
        data += 'set title "B&B tree"\n'
    else:
        data += 'set title "B&B tree (%s %.2fs %s)"\n\n' % (
            self._filename, self._time, self._label)
    for line in additional_lines:
        data += line
    return data
def display(self, item='all', basename='graph', format='png', count=None, pause=False, wait_for_click=True)

Displays/Saves images requested. BranchAndBound method calls this method to visualize the branch and bound tree.

Expand source code
def display(self, item = 'all', basename = 'graph', format='png',
            count=None, pause=False, wait_for_click=True):
    '''
    Displays/Saves images requested. BranchAndBound method calls this method
    to visualize the branch and bound tree.
    '''
    if self.attr['layout'] != 'bak':
        if 'init_log_cond' in self.root.attr:
            max_log_cond = 0
            for n in list(self.nodes.values()):
                if 'init_log_cond' in n.attr:
                    max_log_cond = max(n.attr['init_log_cond'], max_log_cond)
            for n in list(self.nodes.values()):
                if 'init_log_cond' in n.attr:
                    log_begin = n.attr['init_log_cond']
                    log_end = n.attr['final_log_cond']
                    normalized_cond = (1-old_div(log_begin,max_log_cond))
                    color = str(hex(int(normalized_cond*256))[2:]) if normalized_cond >= .0625 else '0' + str(hex(int(normalized_cond*256))[2:])
                    n.attr['label'] = '%.0f \n %.0f' % (log_begin, log_end)
                    n.attr['color'] = '#' + color*3
                    n.attr['fillcolor'] = '#' + color*3
                    n.attr['style'] = 'filled'
                else:
                    n.attr['label'] = ' '
        BinaryTree.display(self, pause = pause, wait_for_click = wait_for_click)
        return
    if self.attr['display'] is 'off':
        return
    if self.attr['display'] is 'matplotlib':
        gnuplot_script = None
        if item=='all':
            self.display_all()
        elif item=='tree':
            gnuplot_script = self.GenerateTreeImage()
        elif item=='scatterplot':
            gnuplot_script = self.GenerateScatterplot()
        elif item=='histogram':
            gnuplot_script = self.GenerateHistogram()
        elif item=='incumbent':
            gnuplot_script = self.GenerateIncumbentPath()
        elif item=='forecast':
            gnuplot_script = self.GenerateForecastImages()
        else:
            raise Exception('Unknown display() method argument %s' %item)
        if gnuplot_script is not None:
            self.display_image(gnuplot_script)
        # clean auxilary files.
        histogram_files = [f for f in os.listdir(".")
                           if f.startswith("histogram")]
        incumbent_files = [f for f in os.listdir(".")
                           if f.startswith("incumbentpath")]
        scatterplot_files = [f for f in os.listdir(".")
                             if f.startswith("scatterplot")]
        t_fathomed_files = [f for f in os.listdir(".")
                            if f.startswith("tree_fathomed")]
        t_infeasible_files = [f for f in os.listdir(".")
                            if f.startswith("tree_infeasible")]
        t_pregnant_files = [f for f in os.listdir(".")
                            if f.startswith("tree_pregnant")]
        t_integer_files = [f for f in os.listdir(".")
                           if f.startswith("tree_integer")]
        t_branched_files = [f for f in os.listdir(".")
                           if f.startswith("tree_branched")]
        bak_filelist = (histogram_files + incumbent_files +
                        scatterplot_files + t_fathomed_files +
                        t_integer_files + t_branched_files +
                        t_infeasible_files + t_pregnant_files)
        for f in bak_filelist:
            os.remove(f)
    elif self.attr['display'] is 'xdot':
        if XDOT_INSTALLED:
            window = xdot.DotWindow()
            window.set_dotcode(self.to_string())
            window.connect('destroy', gtk.main_quit)
            gtk.main()
        else:
            print('Error: xdot not installed. Display disabled.')
            self.attr['display'] = 'off'
    elif self.attr['display'] is 'file':
        if count is not None:
            basename = basename + '_' + str(count)
        if self.attr['layout'] is 'dot2tex':
            if DOT2TEX_INSTALLED:
                if format != 'pdf' or format != 'ps':
                    print("Dot2tex only supports pdf and ps formats,"+\
                        "falling back to pdf")
                    format = 'pdf'
                self.set_layout('dot')
                tex = dot2tex.dot2tex(self.to_string(), autosize=True,
                                      texmode = 'math',
                                      template = DOT2TEX_TEMPLATE)
                f = open(basename+'.tex', 'w')
                f.write(tex)
                f.close()
                subprocess.call(['latex', basename])
                if format == 'ps':
                    subprocess.call(['dvips', basename])
                elif format == 'pdf':
                    subprocess.call(['pdflatex', basename])
                self.set_layout('dot2tex')
                # clean auxilary files.
                aux_filelist = [basename+'.tex', basename+'.log',
                                basename+'.dvi', basename+'.aux']
                for f in aux_filelist:
                    os.remove(f)
            else:
                print("Dot2tex not installed, falling back to graphviz")
                self.set_layout('dot')
                self.write(basename+'.'+format, self.get_layout(), format)
        else:
            gnuplot_script = None
            if item=='all':
                self.display_all()
            elif item=='tree':
                gnuplot_script = self.GenerateTreeImage()
            elif item=='scatterplot':
                gnuplot_script = self.GenerateScatterplot()
            elif item=='histogram':
                gnuplot_script = self.GenerateHistogram()
            elif item=='incumbent':
                gnuplot_script = self.GenerateIncumbentPath()
            elif item=='forecast':
                gnuplot_script = self.GenerateForecastImages()
            else:
                raise Exception('Unknown display() method argument %s' %item)
            if gnuplot_script is not None:
                self.write_image(gnuplot_script, basename+'.'+format)
            # clean auxilary files.
            histogram_files = [f for f in os.listdir(".")
                               if f.startswith("histogram")]
            incumbent_files = [f for f in os.listdir(".")
                               if f.startswith("incumbentpath")]
            scatterplot_files = [f for f in os.listdir(".")
                                 if f.startswith("scatterplot")]
            t_fathomed_files = [f for f in os.listdir(".")
                                if f.startswith("tree_fathomed")]
            t_infeasible_files = [f for f in os.listdir(".")
                                  if f.startswith("tree_infeasible")]
            t_pregnant_files = [f for f in os.listdir(".")
                                if f.startswith("tree_pregnant")]
            t_integer_files = [f for f in os.listdir(".")
                               if f.startswith("tree_integer")]
            t_branched_files = [f for f in os.listdir(".")
                                if f.startswith("tree_branched")]
            bak_filelist = (histogram_files + incumbent_files +
                            scatterplot_files + t_fathomed_files +
                            t_integer_files + t_branched_files +
                            t_infeasible_files + t_pregnant_files)
            for f in bak_filelist:
                os.remove(f)
    else:
        raise Exception('Unknown display mode %s' %self.attr['display'])
def display_all(self)

Assumes all the images have the same size.

Expand source code
def display_all(self):
    '''
    Assumes all the images have the same size.
    '''
    print ('This function is deprected and no longer functions')
    return
def display_image(self, gnuplot_script, pause=False, wait_for_click=True)
Expand source code
def display_image(self, gnuplot_script, pause = False, wait_for_click = True):

    if not (PIL_INSTALLED and MATPLOTLIB_INSTALLED):
        print('Warning: Either matplotlib or Pillow is not installed. Cannot display.')
        return
    
    tmp_fd, tmp_name = tempfile.mkstemp()
    tmp_file = os.fdopen(tmp_fd, 'w+b')
    self.write_image(gnuplot_script, tmp_file)
    tmp_file.close()
    im = PIL_Image.open(tmp_name)
    plt.figure(1)
    plt.clf()
    plt.axis('off')
    plt.imshow(im, interpolation='bilinear' #resample=True
               #extent = (0, 100, 0, 100)
    )
    if wait_for_click == True:
        plt.draw()
        try:
            if plt.waitforbuttonpress(timeout = 10000):
                plt.close()
                exit()
        except:
            exit()
    else:
        plt.show(block=pause)
    im.close()
def process_file(self, file_name)
Expand source code
def process_file(self, file_name):
    self._filename = file_name
    input_file = open(file_name, 'r')
    # Parse all the lines
    for line in input_file:
        self.ProcessLine(line)
        if self.root is not None:
            self.display()
    input_file.close()
def set_display_mode(self, mode)

Api

set_display_mode(self, value)

Description

Sets display mode to value.

Input

value: New display mode.

Post

Display mode attribute of graph is updated.

Expand source code
def set_display_mode(self, mode):
    if mode is 'off':
        self.attr['display'] = mode
    elif mode is 'matplotlib':
        if MATPLOTLIB_INSTALLED:
            self.attr['display'] = 'matplotlib'
        else:
            print('Matplotlib is not installed. Display is set to off.')
            self.attr['display'] = 'off'
    elif mode is 'PIL':
        if PIL_INSTALLED:
            self.attr['display'] = 'PIL'
        else:
            print('PIL is not installed. Display is set to off.')
            self.attr['display'] = 'off'
    elif mode is 'xdot':
        if XDOT_INSTALLED:
            self.attr['display'] = 'xdot'
        else:
            print('Xdot is not installed. Display is set to off.')
            self.attr['display'] = 'off'
    elif mode is 'file':
        self.attr['display'] = 'file'
    elif mode is 'matplotlib':
        self.attr['display'] = 'matplotlib'
    else:
        raise Exception('%s is not a valid display mode.' %mode)
def set_edge_limit(self, limit)
Expand source code
def set_edge_limit(self, limit):
    self._edge_limit = limit
def set_fathom(self, boolean)
Expand source code
def set_fathom(self, boolean):
    self._fathom = boolean
def set_label(self, label)
Expand source code
def set_label(self, label):
    self._label = label
def set_logscaley(self, boolean)
Expand source code
def set_logscaley(self, boolean):
    self._logscaley = boolean
def set_sample_tree(self, number)
Expand source code
def set_sample_tree(self, number):
    self._sample_tree = number
def write_as_dynamic_gexf(self, filename, mode='Dot')
Expand source code
def write_as_dynamic_gexf(self, filename, mode = "Dot"):
    if not GEXF_INSTALLED:
        print('Gexf not installed. Exiting.')
        return
    if mode == 'Dot':
        try:
            gexf = Gexf("Mike O'Sullivan", "Dynamic graph file")
            graph = gexf.addGraph("directed", "dynamic", "Dynamic graph")
            objAtt = graph.addNodeAttribute("obj", "0.0", "float")
            currAtt = graph.addNodeAttribute("current", "1.0",
                                             "integer", "dynamic")
            node_names = self.get_node_list()
            for name in node_names:
                node = self.get_node(name)
                if node.get("step") is None:
                    raise Exception("Node without step in BBTree",
                                    "node =", node)
                curr_step = '%s' % node.get("step")
                next_step = "%s" % (node.get("step") + 1)
                n = graph.addNode(name, node.get_label(), start=curr_step)
                if node.get("obj") is None:
                    raise Exception("Node without objective in BBTree",
                                    "node =", node)
                n.addAttribute(objAtt, "%s" % node.get("obj"))
                n.addAttribute(currAtt, "1", start=curr_step, end=next_step)
                n.addAttribute(currAtt, "0", start=next_step)
            edge_names = self.get_edge_list()
            for i, (m_name, n_name) in enumerate(edge_names):
                edge = self.get_edge(m_name, n_name)
                if edge.get("step") is None:
                    raise Exception("Edge without step in BBTree",
                                    "edge =", (m_name, n_name))
                curr_step = "%s" % edge.get("step")
                graph.addEdge(i, m_name, n_name, start=curr_step)
            output_file = open(filename + ".gexf", "w")
            gexf.write(output_file)
        except Exception as e:
            print(e)
            print("No .gexf file created")
    else:
        raise Exception("Only Dot mode supported in write_as_dynamic_gexf")
def write_image(self, gnuplot_script, filename)
Expand source code
    def write_image(self, gnuplot_script, filename):
        if not (MATPLOTLIB_INSTALLED and PIL_INSTALLED):
            print('Either matplotlib or Pillow is not installed. Display disabled')
            return
        try:
            p = subprocess.run(['gnuplot'], capture_output = True,
                               input = bytearray(gnuplot_script, 'utf8'))
        except OSError:
            print('''Gnuplot executable not found.
Gnuplot must be installed and in your search path.
After installation, ensure that the PATH variable is properly set.''')
            return
        p.check_returncode()

        if p.stderr:
            print(p.stderr)        

        if isinstance(filename, str):
            with open(filename, "w+b") as f:
                f.write(p.stdout)
        else:
            filename.write(p.stdout)