#!/usr/bin/env python
#
# pprof.py, v0.5: Create performance profiles.
# Written by Michael P. Friedlander <michael@mcs.anl.gov>
#
# $Revision: 1.11 $ $Date: 2003-04-24 12:11:21-05 $
#
# I'm still learning Python, so bear with me!
#
import getopt, sys, re
import Gnuplot, Gnuplot.funcutils
from   MA import *
from   string import atoi, atof

PROGNAME = "pprof.py"

def usage():
    instructions = """
Usage: %s [OPTION]... [FILE 1] [FILE 2]...[FILE N]
Create a performance profile chart from metrics in FILEs.
Output is an eps file sent to stdout.

Example 1:
  Profile the metrics in column 3 of the files solver1, solver2, and solver3.
  Use a log2 scale for the x-axis.  Redirect the stdout to profile.eps.
  
    %s -l 2 -c 3 solver1 solver2 solver3 > profile.eps

Example 2:
  Specify a title, linestyle and failure threshold.  Pop up an X window.

    %s -c 3 -t "Plot title" --linestyle "linespoints" \\
             --term "x11" solver1 solver2 solver3

See Dolan and More',
   "Benchmarking optimization software with performance profiles",
   available at http://www-unix.mcs.anl.gov/~more/cops/

Options
  -c, --column=COLUMN      get metrics from column COLUMN (default 1)
  -h, --help               get some help
  -l, --log=BASE           logBASE scale for x-axis (default linear)
      --legend             insert a legend
      --linestyle=STYLE    use STYLE as Gnuplot line style (default steps)
      --sep RE             use regexp RE to indicate new column (default space)
      --term=TERM          use TERM  Gnuplot terminal (default postscript)
  -t, --title=LABEL        use LABEL as  title  (default none)
  -x, --xlabel=LABEL       use LABEL for x-axis (default none)
  -y, --ylabel=LABEL       use LABEL for y-axis (default none)

- Use non-positive values to indicate that the algorithm failed.
- Use --sep 'S' to indicate columns are separated by character S.
- Use --sep "r'RE'" to separate instead by a regular expression.
- Any line starting with a %% or a # is ignored.
""" % (PROGNAME,PROGNAME,PROGNAME)

    print instructions
    sys.exit(0)
    

def commandline_err(msg):
    sys.stderr.write("%s: %s\n" % (PROGNAME, msg))
    sys.stderr.write("Try '%s --help' for more information.\n" % PROGNAME)
    sys.exit(1)


class OptionClass:

    def __init__(self):
	self.datacol  = 1
	self.legend   = None
        self.linestyl = 'steps lw 4'
	self.logscale = None
	self.sep      = '\s+'
        # The GnuPlot postscript driver seems most sophisticated.
        # Also, probably the easiest format for LaTeX to deal with.
	self.term     = 'postscript eps color solid'
	self.title    = None
	self.xlabel   = None
	self.ylabel   = None


def parse_cmdline(arglist):
    """Parse argument list"""

    if len(arglist) == 0: usage()

    options = OptionClass()
    
    try: optlist, files = getopt.getopt(arglist, 'hl:c:x:y:t:',
					["column=", "help",
                                        "legend", "linestyle=",
                                        "sep=",
                                        "log=", "term=", "title=",
					 "xlabel=", "ylabel=",
                                         ])
    except getopt.error, e:
        commandline_err("%s" % str(e))

    if len(files) < 2: usage()

    for opt, arg in optlist:
        if   opt in ("-c", "--column"): options.datacol  = atoi(arg)
        elif opt in ("-h",   "--help"): usage()
	elif opt ==     "--linestyle" : options.linestl  = arg
	elif opt ==        "--legend" : options.legend   = 1
        elif opt in ("-l",    "--log"): options.logscale = atof(arg)
	elif opt ==           "--sep" : options.sep      = arg
	elif opt ==          "--term" : options.term     = arg
	elif opt in ("-t",  "--title"): options.title    = arg
	elif opt in ("-x", "--xlabel"): options.xlabel   = arg
	elif opt in ("-y", "--ylabel"): options.ylabel   = arg

    return (options, files)


class MetricsClass:

    def __init__(self, solvers, opts):
        self.metric  = None
        self.nprobs  = []
        self.perf    = []
        self.solvers = solvers
        self.nsolvs  = len(solvers)
        self.sep     = opts.sep

        map(self.add_solver, solvers)

    def add_solver(self, fname):

        # Reg exp: Any line starting (ignoring white-space)
        # starting with a comment character. Also col sep.
        comment = re.compile(r'^[\s]*[%#]')
        column  = re.compile(self.sep)

        # Grab the column from the file
        metrics = []
        file = open(fname, 'r')
        for line in file.readlines():
            if not comment.match(line):
                line = line.strip()
                cols = column.split( line )
                data = atof(cols[opts.datacol - 1])
		metrics.append(data)
        file.close()

        if self.metric is not None:
	    self.metric = concatenate((self.metric, [metrics]))	    
        else: 
            self.metric = array([metrics])

        # Current num of probs grabbed
        nprobs = len(metrics)
        if not self.nprobs: self.nprobs = nprobs
        elif self.nprobs <> nprobs:
            commandline_error("All files must have same num of problems.")
            
    def prob_mets(self, prob):
        return masked_less_equal(self.metric[:,prob], 0)


class RatioClass:

    def __init__(self, MetricTable):

        # Create empty ratio table
        nprobs = MetricTable.nprobs
        nsolvs = MetricTable.nsolvs
        self.ratios = 1.0 * zeros((nprobs+1, nsolvs))

        # Compute best relative performance ratios across
        # solvers for each problem
        for prob in range(nprobs):
            metrics  = MetricTable.prob_mets(prob)
            best_met = minimum(metrics)
            self.ratios[prob+1,:] = metrics * (1.0 / best_met)

        # Sort each solvers performance ratios
        for solv in range(nsolvs):
            self.ratios[:,solv] = sort(self.ratios[:,solv])

        # Compute largest ratio and use to replace failures entries
        self.maxrat = maximum(self.ratios)
        self.ratios = filled(self.ratios, 1.01 * self.maxrat)

    def solv_ratios(self, solver):
        return self.ratios[:,s]


################
# Main program #
################

opts, solvers = parse_cmdline(sys.argv[1:]) 

metrics = MetricsClass(solvers, opts)
pprofs  = RatioClass(metrics)

nprobs = metrics.nprobs
nsolvs = metrics.nsolvs

# Create a performance profile graph
persist_val = ( opts.term in ('X11', 'x11') )
g = Gnuplot.Gnuplot(persist=persist_val, debug=0)

# Generate the x-axis data
ydata = arange(nprobs+1) * (1.0 / nprobs)

# Set the x-axis ranges
maxrat = pprofs.maxrat + 1.0e-3
if opts.logscale:
    g('set logscale x %f' % opts.logscale)
    minrat = 1
else:
    minrat = 0
g('set xrange [%f:%f]' % (minrat, maxrat / 1.00))

# The y-axis range is fixed
g('set yrange [0:1]')

# Set graph properties
if opts.legend:
    g('set key bottom right')
else:
    g('set nokey')
if opts.term:
    g('set term ' + opts.term)
if opts.title:
    g.title(opts.title)
if opts.xlabel:
    g.xlabel(opts.xlabel)
if opts.ylabel:
    g.ylabel(opts.ylabel)

# Generate arguments for the gplot command
plotargs = []
for s in range(nsolvs):
    sname = solvers[s]
    srats = pprofs.solv_ratios(s)
    plotargs.append(Gnuplot.Data(srats, ydata, title=sname, inline=0,
                                 with=opts.linestyl))

# Create the plot
apply(g.plot, plotargs)

# This fixes a Gnuplot bug. (Thanks to Matt Knepley.)
g.gnuplot.gnuplot.close()

