#!/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
"""
runffx.py v1.3 (Sept 16, 2011)

This module is a toolkit for command-line testing of the Fast Function Extraction (FFX) algorithm.

Reference: Trent McConaghy, FFX: Fast, Scalable, Deterministic Symbolic Regression Technology, Genetic Programming Theory and Practice IX, Edited by R. Riolo, E. Vladislavleva, and J. Moore, Springer, 2011.  http://www.trent.st/ffx
"""

"""
FFX Software Licence Agreement (like BSD, but adapted for non-commercial gain only)

Copyright (c) 2011, Solido Design Automation Inc.  Authored by Trent McConaghy.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
    * Usage does not involve commercial gain. 
    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the associated institutions nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

For permissions beyond the scope of this license, please contact Trent McConaghy (trentmc@solidodesign.com).

THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE DEVELOPERS OR THEIR INSTITUTIONS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

Patent pending.
"""
import csv, os, sys, time, types
import numpy
import ffx

USAGE = """
Fast Function Extraction (FFX) toolkit.

Tools:
  runffx test -- given x/y training data and x/y test data, build model then calculate its test rmse 
  runffx splitdata -- split x/y datafiles into separate datafiles for training and testing
  runffx aboutdata -- gives the number of variables and samples for a given x 
  runffx help -- shows this string
 
Type 'runffx TOOLNAME' with no arguments to get help for that tool, e.g. 'runffx test'.
"""

def splitdata(args):
    help = """
Usage: runffx splitdata INPUTS_FILE[.csv/.txt] OUTPUTS_FILE[.csv/.txt]
   
Given csv-formatted inputs and outputs files, splits them into training and testing data files
of the form INPUTS_FILE_train.csv, OUTPUTS_FILE_train.csv, INPUTS_FILE_test.csv, OUTPUTS_FILE_test.csv.

Sorts the data in ascending y.  Assigns every fourth value to test data; and rest to train data.

In the csv files, there is one column for each sample point.  The inputs files have one 
row for each input variable.  The outputs files have just one row total, because the 
output is scalar.  Values in a given row are separated by spaces.
"""
    #got the right number of args?  If not, output help
    num_args = len(args)
    if num_args != 2:
        print help, '\nGot %d args, need 2.' % num_args; return
    X_file, y_file = args[0], args[1]
    
    if not (X_file.endswith('.csv') or X_file.endswith('.txt')):
        print "INPUTS_FILE file '%s' needs to end with .csv or .txt." % X_file
        return
    if not os.path.exists(X_file):
        print "INPUTS_FILE file '%s' does not exist.  Early exit." % X_file
        return
    if not (y_file.endswith('.csv') or y_file.endswith('.txt')):
        print "OUTPUTS_FILE file '%s' needs to end with .csv or .txt." % y_file
        return
    if not os.path.exists(y_file):
        print "OUTPUTS_FILE file '%s' does not exist.  Early exit." % y_file
        return

    #create the target output filenames, and ensure they don't exist
    join = lambda n, prefix: os.path.join(os.path.dirname(n), prefix+os.path.basename(n))
    train_X_file = join(X_file, 'train_')
    train_y_file = join(y_file, 'train_')
    test_X_file  = join(X_file, 'test_')
    test_y_file  = join(y_file, 'test_')
    for newfile in [train_X_file, train_y_file, test_X_file, test_y_file]:
        if os.path.exists(newfile):
            print "New file '%s' exists, and should not.  Early exit." % newfile; return

    print "Begin runffx splitdata.  INPUTS_FILE.csv=%s, OUTPUTS_FILE.csv=%s" % (X_file, y_file)
    
    #create X, y
    X = read(X_file, dim=2) #[sample_i][var_i] : float
    y = read(y_file, dim=1) #[sample_i] : float
    if X.shape[0] != y.shape[0]:
        X = X.T
    assert X.shape[0] == y.shape[0], 'Error: X shape and y shape do not match. Early exit.'
    
    #create train/test data from X,y
    I = numpy.argsort(y)
    test_I, train_I = [], []
    for (loc, i) in enumerate(I):
        if loc % 4 == 0: test_I.append(i)
        else:            train_I.append(i)
    
    train_X = numpy.take(X, train_I, 0)
    train_y = numpy.take(y, train_I)
    test_X  = numpy.take(X, test_I, 0)
    test_y  = numpy.take(y, test_I)

    print "There will be %d samples in training data, and %d samples in test data" % (len(train_y), len(test_y))

    delimiter = ',' if X_file.endswith('.csv') else '\t'
    numpy.savetxt(train_X_file, train_X, delimiter=delimiter)
    numpy.savetxt(train_y_file, train_y, delimiter=delimiter)
    numpy.savetxt(test_X_file, test_X, delimiter=delimiter)
    numpy.savetxt(test_y_file, test_y, delimiter=delimiter)
    
    print "Created these files:"
    print "  Training inputs:  %s" % train_X_file
    print "  Training outputs: %s" % train_y_file
    print "  Testing inputs:   %s" % test_X_file
    print "  Testing outputs:  %s" % test_y_file
    print "\nDone runffx splitdata."

def aboutdata(args):
    help = """
Usage: runffx aboutdata SAMPLES_IN.csv

Simply prints the number of variables and number of samples for the given ascii database.

"""    
    if len(args) != 1:
        print help, "\nGot %d arguments; need 1." % num_args; return
      
    d = numpy.shape(read(args[0]))
    if len(d) == 1:
        d = 1,d[0]
    print "Data file: %s" % args[0]
    print "Number of input variables: %d" % d[0]
    print "Number of input samples: %d" % d[1]


def testffx(args):
    help = """
Usage: runffx test TRAIN_IN.csv TRAIN_OUT.csv TEST_IN.csv TEST_OUT.csv [VARNAMES.csv]

-Builds a model from training data TRAIN_IN.csv and TRAIN_OUT.csv.
-Computes & prints test nmse using test data TEST_IN.csv TEST_OUT.csv.
  -Also outputs the whole pareto optimal set of # bases vs. error in a .csv

Arguments:
  TRAIN_IN.csv -- model input values for training data
  TRAIN_OUT.csv -- model output values for training data
  TEST_IN.csv -- model input values for testing data
  TEST_OUT.csv -- model output values for testing data
  VARNAMES.csv (optional) -- variable names.  One string for each variable name.

In the training and test files, there is one column for each sample point.  The inputs 
files have one row for each input variable.  The outputs files have just one row total, 
because the output is scalar.  Values in a given row are separated by spaces.
"""
    #got the right number of args?  If not, output help
    num_args = len(args)
    if not (4 <= num_args <= 5):
        print help, '\nGot %d args. Need 4 or 5.' % num_args; return
    print "Begin ffx test."

    #get X/y
    train_X, train_y, test_X, test_y = [read(f,dim) for f,dim in zip(args[:4],[2,1,2,1])] 
    
    #get varnames
    if num_args == 5:
        varnames = read(args[4], dim=1, dtype=str)  
    else:
        varnames = ['x%d' % i for i in xrange(train_X.shape[1])]
    
    #build models
    start_time = time.time()
    models = ffx.run(train_X, train_y, test_X, test_y, varnames)
    
    #output to uniquely-named csv
    output_csv = 'pareto_front_%s.csv' % str(start_time).replace('.','')
    with open(output_csv, 'w') as f:
        f.write('Num bases,Test error (%),Model\n')
        for model in models:
            f.write('%10s, %13s, %s\n' % 
                    ('%d' % model.numBases(), '%.4f' % (model.test_nmse * 100.0), model))
    elapsed_time = time.time() - start_time
    print "Done.  Runtime: %.1f seconds.  Results are in: %s" % \
        (elapsed_time, output_csv)

#=================================================================================
#utility functions
def read(filename, dim=None, **kwargs):
    """Read CSV data from file f. Automatically determines delimiter of source file."""    
    # Auto-determines the entry delimiter
    with open(filename, "rb") as f:
        dialect = csv.Sniffer().sniff(f.read(1024))
    # fix for single-column data, because sniff treats 3.1E09 as 2 fields w/ delim 'E'
    delimiter = ' ' if 'E' in dialect.delimiter else dialect.delimiter
    
    data = numpy.loadtxt(filename, delimiter=delimiter, **kwargs)
    if dim == 1:
        assert data.shape[0] == data.size, 'file %s must be 1-dimensional' % f
    if dim == 2:
        assert len(data.shape) == 2, 'file %s must be 2-dimensional' % f
    return data

#=================================================================================
if __name__ == '__main__':
    if len(sys.argv) == 1:
        print USAGE
        sys.exit(0)
        
    toolname, args = sys.argv[1], sys.argv[2:]
    if toolname == 'help':
        print USAGE
    elif toolname == 'splitdata':
        splitdata(args)
    elif toolname == 'aboutdata':
        aboutdata(args)
    elif toolname == 'test':
        testffx(args)
    else:
        print "There is no toolname of '%s'." % toolname, USAGE
    
    
    