#! /usr/local/bin/python

"Testbench evaluation script."


__author__ = "Johan Vanderhaegen"
__revision__ = "$Id: testbench.py,v 1.9 2002/12/05 06:05:28 jpv Exp $"


import sys,types,string,re,math,os,commands
from getopt import getopt,GetoptError


# ----- terminal support -----

color_codes={}
color_codes["reset"]="\x1b[0m"
color_codes["bold"]="\x1b[01m"
color_codes["teal"]="\x1b[36;06m"
color_codes["turquoise"]="\x1b[36;01m"
color_codes["fuschia"]="\x1b[35;01m"
color_codes["purple"]="\x1b[35;06m"
color_codes["blue"]="\x1b[34;01m"
color_codes["darkblue"]="\x1b[34;06m"
color_codes["yellow"]="\x1b[33;01m"
color_codes["brown"]="\x1b[33;06m"
color_codes["green"]="\x1b[32;01m"
color_codes["darkgreen"]="\x1b[32;06m"
color_codes["red"]="\x1b[31;01m"
color_codes["darkred"]="\x1b[31;06m"

def no_color():
    "turn off colorization"
    for x in color_codes.keys():
	color_codes[x]=""

if not(sys.stdin.isatty()):
    nocolor()

def reset_color():
    return color_codes["reset"]

def ctext(color,text):
    return color_codes[ctext]+text+color_codes["reset"]

def bold(text):
    return color_codes["bold"]+text+color_codes["reset"]

def teal(text):
    return color_codes["teal"]+text+color_codes["reset"]
    
def turquoise(text):
    return color_codes["turquoise"]+text+color_codes["reset"]

def fuschia(text):
    return color_codes["fuschia"]+text+color_codes["reset"]

def blue(text):
    return color_codes["blue"]+text+color_codes["reset"]

def darkblue(text):
    return color_codes["darkblue"]+text+color_codes["reset"]

def green(text):
    return color_codes["green"]+text+color_codes["reset"]

def darkgreen(text):
    return color_codes["darkgreen"]+text+color_codes["reset"]

def white(text):
    return color_codes["bold"]+text+color_codes["reset"]

def yellow(text):
    return color_codes["yellow"]+text+color_codes["reset"]

def red(text):
    return color_codes["red"]+text+color_codes["reset"]



# ----- output -----

def eng(x,digs=0):
    "Format floating number x in engineering notation."
    if x==0:
        n = 0
        exp_sym = ""
    else:
        n = int(math.floor(math.log10(abs(x))))
        digs=int(digs)
        eng_exp = n/3
        if( n > 0 ):
            exp_sym = [" ","K","M","G","T"][n/3]
        else:
            exp_sym = [" ","m","u","n","p","f","a"][-(n/3)]
        x = x/math.pow(10,3*(n/3))
        n = n % 3
    return "%4.*f%s" % (max(digs,3)-n-1,x,exp_sym)
    

def engtof(str):
    "Convert num formatted in engineering notation to a fp number."
    eng_exp = { "k":1e+3,"x":1e+6,"g":1e+9,"t":1e+12,
                "m":1e-3,"u":1e-6,"n":1e-9,"p":1e-12,"f":1e-15 }
    m = re.match(r"(?i)^\s*([+-]?\d+(?:\.\d*)?)([KXGTmunpf])\s*$",str)
    if m:
        num = float(m.group(1)) \
              *eng_exp[string.lower(m.group(2))]
    else:
        num = float(str)
    return num


def check_result(name,result,test,spec=""):
    "Check if result meets spec and print info."

    # convert result to a numeric value
    # and evaluate 'test' on numeric result
    try:
        if type(result) is types.StringType:
            result_num = engtof(result)
        elif type(result) is types.FloatType:
            result_num = result
        else:
            raise ValueError
    except ValueError:
        result_num = 0              # should be NaN
        result_str = result
        status = 0
        status_str = red("FUBAR")
    else:
        result_str = eng(result_num)
        status = not(not( apply(test,[result_num]) ))
        if status:
            status_str = green("Ok")
        else:
            status_str = red("Failed")
    
    # print and return results
    if type(spec) is types.FloatType:
        spec_str = eng(spec)
    elif type(spec) is types.StringType:
        spec_str = spec
    else:
        spec_str = str(spec)
    
    print "%-60s [ %s ]" %(
        "%-35s (spec: %s)" % ("  %-20s: %s" %(name,result_str),spec_str),
        status_str)
    return result_num,status


def print_result(name,result,do_print):
    "Print result info."
    try:
        result_num = engtof(result)
    except ValueError:
        result_num = 0                  # should be NaN
        result_str = result
    else:
        result_str = eng(result_num)
    if do_print:
        print "  %-20s: %s" %(name,result_str)
    return result_num


def get_item(list,n):
    "Returns an item from a list with range checking."
    if len(list) > n:
        return list[n]
    else:
        return "fubar"


def convert_results(x):
    "Converts a tuple/list of string to a tuple/list numerical values."
    if type(x) is types.TupleType:
        z = ()                          # handle tuples
        for y in x:
            try:
                zz = convert_results(y)
            except ValueError:
                pass
            else:
                z = z + (zz,)
        return z
    elif type(x) is types.ListType:
        z = []                          # handle lists
        for y in x:
            try:
                zz = convert_results(y)
            except ValueError:
                pass
            else:
                z = z + [zz]
        return z
    elif type(x) is types.StringType:   # convert strings
        return engtof(x)
    else:
        raise "Wrong argument type"
    

# ----- check_circuit -----

def check_circuit():
    pass


# ----- eval_testbench -----

def eval_testbench1(results,status,diag):
    "Evaluate testbench 1 results."
    
    # initialize
    print fuschia("\n*** Testbench 1 ***\n")
    try:
        lis = open("testbench1.lis").read()
    except IOError,e:
        sys.stderr.write(
            "Unable to open file %s for reading: %s.\n"
            "... Skipping ...\n" %(
            "testbench1.lis",e.strerror))
        return results,status
    
    # power consumption
#    match = re.findall(
#        r"(?im)^\s*total voltage source power dissipation\s*=\s*(\S+)",lis)
#    results["power"] = print_result(
#        "power consumption",get_item(match,0),1)
#    results["powerTT"],status["powerTT"] = check_result(
#        "power consumption (TT)",get_item(match,0),
#        lambda x: x<1.2e-3, 1.2e-3)
#    results["powerFF"],status["powerFF"] = check_result(
#        "power consumption (FF)",get_item(match,2),
#        lambda x: x<1.2e-3, 1.2e-3)
#    results["powerSS"],status["powerSS"] = check_result(
#        "power consumption (SS)",get_item(match,4),
#        lambda x: x<1.2e-3, 1.2e-3)

     # power consumption
    match = re.findall(r"(?im)^\s*ivdd\s*=\s*(\S+)",lis)
    results["ivdd_posTT"],status["ivdd_posTT"] = check_result(
        "current consumption (TT, pos)",get_item(match,0),
        lambda x: x<1e-3, 1e-3)
    results["ivdd_negTT"],status["ivdd_negTT"] = check_result(
        "current consumption (TT, neg)",get_item(match,1),
        lambda x: x<1e-3, 1e-3)
    results["ivdd_posFF"],status["ivdd_posFF"] = check_result(
        "current consumption (FF,pos)",get_item(match,2),
        lambda x: x<1e-3, 1e-3)
    results["ivdd_negFF"],status["ivdd_negFF"] = check_result(
        "current consumption (FF,neg)",get_item(match,3),
        lambda x: x<1e-3, 1e-3)
    results["ivdd_posSS"],status["ivdd_posSS"] = check_result(
        "current consumption (SS,pos)",get_item(match,4),
        lambda x: x<1e-3, 1e-3)
    results["ivdd_negSS"],status["ivdd_negSS"] = check_result(
        "current consumption (SS,neg)",get_item(match,5),
        lambda x: x<1e-3, 1e-3)
    
    
   
    
    # settling error
    match = re.findall(r"(?im)^\s*verrfinal\s*=\s*(\S+)",lis)
    results["verr_posTT"],status["verr_posTT"] = check_result(
        "settling error (V) (TT, pos)",get_item(match,0),
        lambda x: x<60e-6, 60e-6)
    results["verr_negTT"],status["verr_negTT"] = check_result(
        "settling error (V) (TT, neg)",get_item(match,1),
        lambda x: x<60e-6, 60e-6)
    results["verr_posFF"],status["verr_posFF"] = check_result(
        "settling error (V) (FF,pos)",get_item(match,2),
        lambda x: x<60e-6, 60e-6)
    results["verr_negFF"],status["verr_negFF"] = check_result(
        "settling error (V) (FF,neg)",get_item(match,3),
        lambda x: x<60e-6, 60e-6)
    results["verr_posSS"],status["verr_posSS"] = check_result(
        "settling error (V) (SS,pos)",get_item(match,4),
        lambda x: x<60e-6, 60e-6)
    results["verr_negSS"],status["verr_negSS"] = check_result(
        "settling error (V) (SS,neg)",get_item(match,5),
        lambda x: x<60e-6, 60e-6)
    
    

    # settling time
    match = re.findall(r"(?im)^\s*tsettle\s*=\s*(\S+)",lis)
#    tsettle = convert_results(match)
    results["settling_posTT"] = print_result(
        "settling time (TT, pos)",get_item(match,0),1)
    results["settling_negTT"] = print_result(
        "settling time (TT, neg)",get_item(match,1),1)
    results["settling_posFF"] = print_result(
        "settling time (FF, pos)",get_item(match,2),1)
    results["settling_negFF"] = print_result(
        "settling time (FF, neg)",get_item(match,3),1)
    results["settling_posSS"] = print_result(
        "settling time (SS, pos)",get_item(match,4),1)
    results["settling_negSS"] = print_result(
        "settling time (SS, neg)",get_item(match,5),1)

    # return results
    return results,status


def eval_testbench2(results,status,diag):
    "Evaluate testbench 2 results."
    
    # initialize
    print fuschia("\n*** Testbench 2 ***\n")
    try:
        lis = open("testbench2.lis").read()
    except IOError,e:
        sys.stderr.write(
            "Unable to open file %s for reading: %s.\n"
            "... Skipping ...\n" %(
            "testbench2.lis",e.strerrror))
        return results,status
    
    # closed loop frequency domain results
    match = re.findall(r"(?im)^\s*a0\s*=\s*(\S+)",lis)
    results["a0_TT"] = print_result(
        "closed loop a0 (TT)",get_item(match,0),diag)
    results["a0_FF"] = print_result(
        "closed loop a0 (FF)",get_item(match,1),diag)
    results["a0_SS"] = print_result(
        "closed loop a0 (SS)",get_item(match,2),diag)

    match = re.findall(r"(?im)^\s*f3db\s*=\s*(\S+)",lis)
    results["f3db_TT"] = print_result(
        "closed loop f3db (TT)",get_item(match,0),diag)
    results["f3db_FF"] = print_result(
        "closed loop f3db (FF)",get_item(match,1),diag)
    results["f3db_SS"] = print_result(
        "closed loop f3db (SS)",get_item(match,2),diag)
    
    
    # return results
    return results,status


def eval_testbench3(results,status,diag):
    "Evaluate testbench 3 results."
    
    # initialize
    print fuschia("\n*** Testbench 3 ***\n")
    try:
        lis = open("testbench3.lis").read()
    except IOError,e:
        sys.stderr.write(
            "Unable to open file %s for reading: %s.\n"
            "... Skipping ...\n" %(
            "testbench3.lis",e.strerror))
        return results,status
    
    # open loop differential mode results
    match = re.findall(r"(?im)^\s*v\(out\)/vid\s*=\s*(\S+)",lis)
    results["Adm0_TT"] = print_result(
        "Adm0 (TT)",get_item(match,0),1)
    results["Adm0_FF"] = print_result(
        "Adm0 (FF)",get_item(match,1),1)
    results["Adm0_SS"] = print_result(
        "Adm0 (SS)",get_item(match,2),1)

    match = re.findall(r"(?im)^\s*fug\s*=\s*(\S+)",lis)
    results["Adm_fug_TT"] = print_result(
        "Adm unity gain freq (TT)", get_item(match,0),1)
    results["Adm_fug_FF"] = print_result(
        "Adm unity gain freq (FF)", get_item(match,1),1)
    results["Adm_fug_SS"] = print_result(
        "Adm unity gain freq (SS)", get_item(match,2),1)
   
    match = re.findall(r"(?im)^\s*phim\s*=\s*(\S+)",lis)
    results["Adm_phim_TT"] = print_result(
        "Adm phase margin--degrees (TT)", get_item(match,0),1)
    results["Adm_phim_FF"] = print_result(
        "Adm phase margin--degrees (FF)", get_item(match,1),1)
    results["Adm_phim_SS"] = print_result(
        "Adm phase margin--degrees (SS)", get_item(match,2),1)
   
    match = re.findall(r"(?im)^\s*gainm\s*=\s*(\S+)",lis)
    results["Adm_gainm_TT"] = print_result(
        "Adm gain margin--dB (TT)", get_item(match,0),1)
    results["Adm_gainm_FF"] = print_result(
        "Adm gain margin--dB (FF)", get_item(match,1),1)
    results["Adm_gainm_SS"] = print_result(
        "Adm gain margin--dB (SS)", get_item(match,2),1)
   
    # return results
    return results,status


def eval_testbench3a(results,status,diag):
    "Evaluate testbench 3a results."
    
    # initialize
    print fuschia("\n*** Testbench 3a ***\n")
    try:
        lis = open("testbench3a.lis").read()
    except IOError,e:
        sys.stderr.write(
            "Unable to open file %s for reading: %s.\n"
            "... Skipping ...\n" %(
            "testbench3a.lis",e.strerror))
        return results,status
    
    # open loop differential mode results
    match = re.findall(r"(?im)^\s*v\(out_ref\)/vid_ref\s*=\s*(\S+)",lis)
    results["Adm0_ref"],status["Adm0_ref"] = check_result(
        "Adm0",get_item(match,0),
        lambda x: abs(x) >= 2000, 2000 )
    match = re.findall(r"(?im)^\s*v\(out_low\)/vid_low\s*=\s*(\S+)",lis)
    results["Adm0_low"],status["Adm0_low"] = check_result(
        "Adm0 (low output)",get_item(match,0),
        lambda x: abs(x) >= 2000, 2000 )
    match = re.findall(r"(?im)^\s*v\(out_high\)/vid_high\s*=\s*(\S+)",lis)
    results["Adm0_high"],status["Adm0_high"] = check_result(
        "Adm0 (high output)",get_item(match,0),
        lambda x: abs(x) >= 2000, 2000 )
    
    # return results
    return results,status


def eval_testbench4(results,status,diag):
    "Evaluate testbench 4 results."
    
    # initialize
    print fuschia("\n*** Testbench 4 ***\n")
    try:
        lis = open("testbench4.lis").read()
    except IOError,e:
        sys.stderr.write(
            "Unable to open file %s for reading: %s.\n"
            "... Skipping ...\n" %(
            "testbench4.lis",e.strerror))
        return results,status
    
    # open loop common mode results
    match = re.findall(r"(?im)^\s*v\(out\)/vic\s*=\s*(\S+)",lis)
    results["Acm0_TT"],status["Acm0_TT"] = check_result(
        "Acm0 (TT)",get_item(match,0),
        lambda x: abs(x) <= 0.1, 0.1)
    results["Acm0_FF"],status["Acm0_FF"] = check_result(
        "Acm0 (FF)",get_item(match,1),
        lambda x: abs(x) <= 0.1, 0.1)
    results["Acm0_SS"],status["Acm0_SS"], = check_result(
        "Acm0 (SS)",get_item(match,2),
        lambda x: abs(x) <= 0.1, 0.1)

    # return results
    return results,status


def eval_testbench5(results,status,diag):
    return results,status

def eval_testbench6(results,status,diag):
    return results,status


# ----- run_hspice -----

def run_hspice(testbench):
    """Runs HSPICE for the specified testbench.

    The testbench is supposed to be named <testbench>.sp
    and the output will be named <testbench>.lis ...
    """
    
    print "Running %s . . ." %testbench
    (status,output) = commands.getstatusoutput(
        "hspice %s -o %s" % (testbench,testbench) )
    if status:
        sys.stderr.write("Failed to simulate %s:\n%s\n" % (testbench,output))
        sys.exit(2)
    print "  done."
    

# ----- main -----

def print_usage(exit_code):
    "Print script usage to stderr and exits with exit code <exit_code>."

    sys.stderr.write("""Testbench evaluation script.

Usage: Testbench [options] [testbenches]

     [testbenches] is a list of testbenches
     (e.g. 'testbench1 testbench3')

     Omitting [testbenches] will automatically select all testbenches.

Options:
     -h,--help       Prints out this message
     -c,--nocolor    Disables the use of color in the output
     -r,--run        Run testbench(es)
     -e,--eval       Evaluate testbench result(s)   (= default action)
     -d,--diag       Evaluate circuit diagnostic results also
                     when evaluating testbench results

""")
    sys.exit(exit_code)
    

def main(args):
    """Main script entry.
    
    <args> are the command line arguments,
    except the one which contains the executable name.
    """
    
    try:
        (opts,args) = getopt(
            args, "hcred", [
            "help","nocolor","run","eval","diag" ]);
    except GetoptError,e:
        sys.stderr.write(
            "Error while parsing command line options:\n %s.\n\n" %(
            e.msg))
        print_usage(1)
    
    actions = []
    diag = 0
    for (opt,val) in opts:
        if opt in ('-h','--help'):
            print_usage(0)
        elif opt in ('-c','--nocolor'):
            no_color()
        elif opt in ('-r','--run'):
            actions.append('run')
        elif opt in ('-e','--eval'):
            actions.append('eval')
        elif opt in ('-d','--diag'):
            diag = 1
        else:
            assert 0, "Unhandled option: %s" % opt
    
    if not(actions):
        actions = ['eval']              # default actions
    
#    testbenches = ["testbench1","testbench2","testbench3","testbench3a",
#                   "testbench4","testbench5","testbench6"]
    testbenches = ["testbench1","testbench2", "testbench3","testbench4"]
    
    if not(args):
        args = testbenches              # default testbenches
    
    for arg in args:                    # check testbenches
        if arg not in testbenches:
            sys.stderr.write("Unknown testbench: %s\n" % arg)
            sys.exit(1)
    
    
    # run testbenches and/or evaluate testbench results
    
    if 'run' in actions:
        for tb in testbenches:
            if tb in args:
                run_hspice(tb)
    
    results = {}
    status = {}
    if 'eval' in actions:
        for tb in testbenches:
            if tb in args:
                results,status = apply(
                    eval("eval_%s" % tb), [results,status,diag] )
    print
    

# ----- __main__ -----

if __name__ == "__main__":
    main(sys.argv[1:])
