// script irGrade:  interactive, real-time grading; html formatting; statistical functions,
//                  linear algebra
// copyright 1997, 1998, 1999, 2000, 2001. P.B. Stark, stark@stat.berkeley.edu
// Version 1.0
// All rights reserved.

// !!!!Beginning of the code!!!!

var irGradeModTime = '2001/10/09/0150';
                                   // modification date and time
var today = (new Date()).toLocaleString();
var copyYr = '1997--2001. ';       // copyright years
var theChapters = [-1,0,1,2,3.1,3.2,3.3,4.1,4.2,4.3,5.1,5.2,5.3,6,7,8.1,8.2,9,
                   10,11,12,13,14.1,14.2,14.3,15,16.1,16.2,17.1,17.2,18,19.1,19.2,
                   20.1,20.2,20.3,21,22,23,24,25
                  ];               // list of the chapters
var cookieExpireDays = 10;         // days for the cookies to endure
var theChapter;                    // current chapter
var type = 'LA';                   // default question type is 'literal answer'
var newStyleAnswer = true;         // flag for pop-up versus inline
var ansText = ' ';                 // to display correct answer
var pCtr = 1;                      // counter for problems
var qCtr = 1;                      // counter for questions
//var sCtr = 0;                      // counter for solved exercises
var fCtr = 0;                      // counter for footnotes
//var sEx = new Array();             // array for exercise numbers of solved exercises
//var sEnum = 0;                     // counter for displaying solutions
var footnotes = new Array();       // array of footnotes
var key = new Array();             // key for self-graded exercises
var boxList = new Array();         // list of images for self-graded exercises
var longKey = new Array();         // long answers for exercises
var solutionCode = new Array();    // javascript code to evaluate after writing solution
var setNum;                        // current problem set number
var isLab = false;                 // is this a problem set?
var pbsURL = 'http://www.stat.berkeley.edu/~stark';
                                   // P.B. Stark's URL
var pbsRef = '<a href="' + pbsURL + '" target="_top" ' +
    ' onmouseout="window.status=defaultStatus;return(true);" ' +
    ' onmouseover="window.status=\'P.B. Stark\';return(true);">P.B. Stark</a>';
                                   // link to author
var fudgeFactor = 0.01;            // relative tolerance for imprecise numerical answers
var absFudge = 1.e-20;             // absolute tolerance for identically zero answers
var wrongHtm = '<p><center><font size="+1" color="red">Sorry, wrong answer.</font></center></p>';
                           // message for wrong answer
var rightHtm = '<p><center><font size="+1" color="green">Correct!</font></center></p>';
                           // message for right answer
var dsmsHtm =  '<p><center> <input type="button" name="dismiss" value="Close" ' +
        ' onClick="self.close()"></center></p>';
                           // to go on when answer is right
var startHtm = '<html> <head></head> <body><form>';
var endHtm = '</form></body> </html>';
var showAnsHtm = '<p><center> <input type="button" ' +
        'name="tryAgain" value="Try again" ' +
        ' onClick="self.close()"></center></p>' +
        '<p><center> <input type="button" ' +
        'name="showMe" value="Show me the answer" ' +
        'onClick="document.clear();document.write(htmStuff);" ' +
        ' </center></p>';       // to go on or show answer when answer is wrong
var inlinePrefix="Q#:a#:";
var bigPi = "3141592653";
var rmin = 2.3e-308;            // for numerical analysis
var eps = 2.3e-16;              // ditto
var maxIterations = 100;        // default iteration limit for iterative algorithms
var continueLab;
var htmStuff;
var randSeed;                   // seed of random number generator
var CA = false;
var sectionContext;             // store chapter-specific initialization script
var qImgSrc = '../Graphics/answer_unknown.gif';
var rightImgSrc = '../Graphics/answer_good.gif';
var wrongImgSrc = '../Graphics/answer_bad.gif';
var alphabet = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p',
        'q','r','s','t','u','v','w','x','y','z'
        ];
var ALPHABET = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
        'Q','R','S','T','U','V','W','X','Y','Z'
        ];
var Alphabet = ALPHABET;
var cardinals = ['zero','one','two','three','four','five','six','seven','eight','nine','ten',
            'eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen',
            'nineteen','twenty','twenty-one','twenty-two','twenty-three','twenty-four',
            'twenty-five','twenty-six','twenty-seven','twenty-eight','twenty-nine','thirty',
            'thirty-one','thirty-two','thirty-three','thirty-four','thirty-five','thirty-six',
            'thirty-seven','thirty-eight','thirty-nine','fourty','fourty-one','fourty-two',
            'fourty-three','fourty-four','fourty-five','fourty-six','fourty-seven','fourty-eight',
            'fourty-nine','fifty','fifty-one','fifty-two','fifty-three','fifty-four','fifty-five',
            'fifty-six','fifty-seven','fifty-eight','fifty-nine','sixty','sixty-one','sixty-two',
            'sixty-three','sixty-four','sixty-five','sixty-six','sixty-seven','sixty-eight',
            'sixty-nine','seventy','seventy-one','seventy-two','seventy-three','seventy-four',
            'seventy-five','seventy-six','seventy-seven','seventy-eight','seventy-nine',
            'eighty','eighty-one','eighty-two','eighty-three','eighty-four','eighty-five',
            'eighty-six','eighty-seven','eighty-eight','eighty-nine','ninety',
            'ninety-one','ninety-two','ninety-three','ninety-four','ninety-five',
            'ninety-six','ninety-seven','ninety-eight','ninety-nine','one hundred'
            ];
var Cardinals = ['Zero','One','Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten',
            'Eleven','Twelve','Thirteen','Fourteen','Fifteen','Sixteen','Seventeen','Eighteen',
            'Nineteen','Twenty','Twenty-one','Twenty-two','Twenty-three','Twenty-four',
            'Twenty-five','Twenty-six','Twenty-seven','Twenty-eight','Twenty-nine','Thirty',
            'Thirty-one','Thirty-two','Thirty-three','Thirty-four','Thirty-five','Thirty-six',
            'Thirty-seven','Thirty-eight','Thirty-nine','Fourty','Fourty-one','Fourty-two',
            'Fourty-three','Fourty-four','Fourty-five','Fourty-six','Fourty-seven','Fourty-eight',
            'Fourty-nine','Fifty','Fifty-one','Fifty-two','Fifty-three','Fifty-four','Fifty-five',
            'Fifty-six','Fifty-seven','Fifty-eight','Fifty-nine','Sixty','Sixty-one','Sixty-two',
            'Sixty-three','Sixty-four','Sixty-five','Sixty-six','Sixty-seven','Sixty-eight',
            'Sixty-nine','Seventy','Seventy-one','Seventy-two','Seventy-three','Seventy-four',
            'Seventy-five','Seventy-six','Seventy-seven','Seventy-eight','Seventy-nine',
            'Eighty','Eighty-one','Eighty-two','Eighty-three','Eighty-four','Eighty-five',
            'Eighty-six','Eighty-seven','Eighty-eight','Eighty-nine','Ninety',
            'Ninety-one','Ninety-two','Ninety-three','Ninety-four','Ninety-five',
            'Ninety-six','Ninety-seven','Ninety-eight','Ninety-nine','One hundred'
            ];
var ordinals = ['zeroth','first','second','third','fourth','fifth','sixth','seventh','eighth',
            'ninth','tenth','eleventh','twelfth','thirteenth','fourteenth','fifteenth',
            'sixteenth','seventeenth','eighteenth','ninteenth','twentieth','twenty-first',
            'twenty-second','twenty-third','twenty-fourth','twenty-fifth','twenty-sixth',
            'twenty-seventh','twenty-eighth','twenty-ninth','thirtieth','thirty-first',
            'thirty-second','thirty-third','thirty-fourth','thirty-fifth','thirty-sixth',
            'thirty-seventh','thirty-eighth','thirty-ninth','fourtieth','fourty-first',
            'fourty-second','fourty-third','fourty-fourth','fourty-fifth','fourty-sixth',
            'fourty-seventh','fourty-eighth','fourty-ninth','fiftieth','fifty-first',
            'fifty-second','fifty-third','fifty-fourth','fifty-fifth','fifty-sixth',
            'fifty-seventh','fifty-eighth','fifty-ninth','sixtieth','sixty-first',
            'sixty-second','sixty-third','sixty-fourth','sixty-fifth','sixty-sixth',
            'sixty-seventh','sixty-eighth','sixty-ninth','seventieth','seventy-first',
            'seventy-second','seventy-third','seventy-fourth','seventy-fifth','seventy-sixth',
            'seventy-seventh','seventy-eighth','seventy-ninth','eightieth','eighty-first',
            'eighty-second','eighty-third','eighty-fourth','eighty-fifth',
            'eighty-sixth','eighty-seventh','eighty-eightth','eighty-ninth','ninetieth',
            'ninety-first','ninety-second','ninety-third','ninety-fourth','ninety-fifth',
            'ninety-sixth','ninety-seventh','ninety-eighth','ninety-ninth','hundredth'
            ];
var iteratives = ['no times','once','twice','thrice'];
for (var i=4; i < cardinals.length; i++) {
    iteratives[i] = cardinals[i] + ' times';
}
var primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53];
var nPrimes = [0, 0, 1, 2, 2, 3, 3, 4, 4, 4,           // 0-9
               4, 5, 5, 6, 6, 6, 6, 7, 7, 8,           // 10-19
               8, 8, 8, 9, 9, 9, 9, 9, 9, 10,          // 20-29
               10, 11, 11, 11, 11, 11, 11, 12, 12, 12, // 30-39
               12, 13, 13, 14, 14, 14, 14, 15, 15, 15, // 40-49
               15, 15, 15, 16];                        // 50-53
var allTheChars = '~\`!1@2#3$4%5^6&7*8(9)0_-+=QqWwEeRrTtYyUuIiOoPp{[}]|\\AaSsDdFfGgHhJjKkLl:;' +
                  '\"\'ZzXxCcVvBbNnMm<,>.?/';
var faces = ['Ace','two','three','four','five','six','seven','eight','nine','ten','Jack',
         'Queen','King'];
var suits = ['spades','hearts','diamonds','clubs'];
var colors = ['red','orange','yellow','green','blue','indigo','violet',
              'black','white','gray','silver','gold','brown','aqua','teal',
              'fuschia','magenta','cyan','sage','turquoise','chartreuse',
              'mauve','periwinkle','umber','brick','marigold','seafoam','coral',
              'purple','grape','cherry','beige','copper','sienna','baby blue'
             ];

// ========================================================================
//  FUNCTION LIBRARY
// ========================================================================

// ===============  STRING HANDLERS and HTML GENERATORS ===================

function trimBlanks(s){
    if (s == null || s.length == 0 ) { return(s); }
    while (s.charAt(s.length-1) == ' ' ) {      // trim trailing blanks
        s = s.substring(0, s.length-1);
    }
    while (s.charAt(0) == ' ') {                // trim leading blanks
        s = s.substring(1, s.length);
    }
    return(s);
}

function allBlanks(s) {
    if (s == null || trimBlanks(s).length == 0) {
        return(true);
    } else {
        return(false);
    }
}

function removeAllBlanks(s){
    if (s == null || s.length == 0 ) {
        return(s);
    }
    while (s.indexOf(' ') > -1 ) {
        s = s.substring(0, s.indexOf(' ')) + s.substring(s.indexOf(' ')+1, s.length);
    }
    return(s);
}

function makeTopLevel(nameString,features) {
   // ensures the window is a top level window with name nameString
    if (window != top) {
        window.open(window.location,nameString,features);
        window.close();
        parent.close();
    } else {
        window.name=nameString;
    }
    return(true);
}

function makeTopLevel(nameString){
    if (window != top) {
        window.open(window.location,nameString,
            'toolbar=yes,location=yes,status=yes');
        window.close();
        parent.close();
    } else window.name=nameString;
    return(true);
}

function makeTopLevel(){
    if (window != top) {
        window.open(window.location,'',
            'toolbar=yes,location=yes,status=yes');
        window.close();
        parent.close();
    }
    return(true);
}

function sticiRef() {
  // pops up a  window with the pronunciation of  SticiGui
    sticiWin =  window.open('','sticiWin',
                'toolbar=no,location=no,directories=no,status=no,scrollbars=yes,'+
                'resizable=yes,width=200,height=30,top=5,left=5');
    sticiWin.document.write('<html><head><title>How do you say that?</title>' +
            '</head><body><script language="JavaScript">' +
            'setTimeout("window.close()",3000)' +
            '</script><h3 align=\"center\">Pronounced</h3>' +
            '<h3 align=\"center\">\"Sticky-Gooey\"</h3>' +
            '</body></html>');
    sticiWin.document.close();
    sticiWin.focus();
    return(true);
}

function glossRef(lnk) {
// opens a glossary window, if not already open, and displays the glossary entry
// referred to in the link.
    window.open(lnk, target="glossWin");
    return(true);
}

function trimToLowerCase(s) {
// trim trailing blanks, convert to lower case
    if (s == null || s.length == 0 ) {
        return(s);
    }
    return(trimBlanks(s.toLowerCase()));
}

function allLetters(s){
// return true if all characters in string s are letters (or trailing blanks)
    var trimS = trimToLowerCase(s);
    var alpha=' -abcdefghijklmnopqrstuvwxyz';
    var truth = true;
    for (var i = 0; i < trimS.length; i++) {
        if(alpha.indexOf(trimS.charAt(i)) < 0) {
            truth = false;
        }
    }
    return(truth);
}

function removeCommas(s) { // removes commas from a putative number
    return(removeString(',',s));
}

function removeString(str,s) { // removes instances of the string str from s.
                               // kluge to avoid regular expressions
    var sOut = s;
    var s1;
    var strLen = str.length;
    var inx = s.indexOf(str);
    while (inx > -1) {
        s1 = s.substring(0,inx);
        if (inx < (s.length - strLen) ) {
            s1 += s.substring(inx+strLen, s.length);
        }
        s = s1;
        inx = s.indexOf(str);
    }
    return(s);
}

function removeStrings(strArr,s) {
    for (var j=0; j < strArr.length; j++) {
        s = removeString(strArr[j], s);
    }
    return(s);
}

function parsePercent(s) {
// parse a number that contains a % sign to turn it into a decimal fraction
    var value;
    if (s.indexOf('%') == -1) {
        value = parseFloat(trimBlanks(removeCommas(s)))
    } else {
        while (s.indexOf('%') != -1) {
            s = s.substring(0,s.indexOf('%')) +
                s.substring(s.indexOf('%')+1,s.length)
        }
        value = parseFloat(trimBlanks(removeCommas(s)))/100;
    }
    return(value);
}

function evalNum(s) { // try to evaluate a string as a numeric value
    var value;
    var dmy = s + ' ';
    dmy = dmy.replace(/\%/g,'\/100');
    dmy = dmy.replace(/,/g,'');
    if ( typeof(s) == 'undefined' || s == null || !s.match(/[^ ]/) ) {
        value = 'NaN';
    } else if ( dmy.match(/[^1234567890+*\/.ed() -]/i) ) {
        value = 'NaN';
    } else if ( !dmy.match(/[1234567890]/) ) {
        value = 'NaN';
    } else {
        eval('value = ' + dmy + ';');
    }
    return(value);
}

function parseMultiple(option) {
  // pre-processes multiple selections so that checkAnswer can be used to grade them
    var response = '';
    for (var i=0; i < option.length; i++) {
        if (option[i].selected) {
            response += trimToLowerCase(option[i].value) + ',' ;
        }
    }
    if (response.charAt(response.length - 1) == ',') {  // trim trailing comma
        response = response.substring(0, response.length - 1);
    }
    return(response);
}

function getFormElementIndex(q) {
 // finds the form element named q and returns its index.
    for (var inx =0; inx < document.forms[0].elements.length; inx++) {
        if (document.forms[0].elements[inx].name == q)
        return(inx);
    }
    alert('Error #1 in irGrade.getFormElementIndex(): Form element ' + q + ' is missing!');
    return('document.forms[0].length');
}

function findNum(s) {
// if s is an integer or its string representation, returns s.
// if not, tries to remove characters to
// leave an int, and returns that int.
    var i = parseInt(s);
    if ( !isNaN(i)) {
        return(i);
    } else {
        var q = '';
        for (var j=0; j < s.length; j++) {
            var dum = q;
            if (!isNaN(parseInt(dum + s.charAt(j)))) {
                q += s.charAt(j);
            }
        }
        return(q);
    }
}

function vFindNum(s) { // finds numbers in a string array
    var a = new Array(s.length);
    for (var i=0; i < s.length; i++) { a[i] = findNum(s[i]);}
    return(a);
}

// ============================================================================
// ========================= COOKIE MANIPULATION ==============================

function expireTimeString(ed) {
    var now = new Date();
    var expire = new Date();
    var cookDays;
    if (typeof(ed) == 'undefined' || ed == null) {
        cookDays = cookieExpireDays;
    } else {
        cookDays = ed;
    }
    expire.setTime(now.getTime() + cookDays*60*60*1000*24);
    return(expire.toGMTString());
}

function getCookieVal(cook,key){ // gets the value of key within the cookie cook
    var searchStr = cook + '&';
    var val = null;
    var pat = key + '=';
    var inx = searchStr.indexOf(pat);
    if (inx > -1){
        searchStr = searchStr.substring(inx + pat.length, searchStr.length);
        val = unescape(searchStr.substring(0, searchStr.indexOf('&')));
    }
    return(val);
}

function getCookieArray(cook,stem,len) { // gets an array from the cookie
    var ansArr = new Array(len);
    for (var i=0; i < len; i++) {
        ansArr[i] = getCookieVal(cook, stem + i.toString());
    }
    return(ansArr);
}

function setCookieArray(arr,stem) { // make a cookie-like string from the array arr
    var ansStr = '';
    for (var i=0; i < arr.length; i++) {
        ansStr += stem + i.toString() + '=' + arr[i] + '&';
    }
    if (ansStr.substr(ansStr.length-1,1) == '&') {
        ansStr = ansStr.substring(0, ansStr.length - 1);
    }
    return(ansStr);
}

// ============================================================================
// ========================= PROBLEM AND GRADING SUBROUTINES ==================

function numToMultiple(opt,ans) { // finds the multiple choice closest to ans
    var dif = Math.abs(parsePercent(opt[0]) - ans);
    var aVal = 'a';
    for (var i=1; i < opt.length; i++) {
        var d2 = Math.abs(parsePercent(opt[i]) - ans);
        if (d2 < dif) {
            dif = d2;
            aVal = alphabet[i];
        }
    }
    return(aVal);
}

function hiddenInput(string, val){  // hidden input with name="string" and value="val"
    var s = "<input type=\"hidden\" name=\"" + string + "\" value=\"" + val + "\">";
    return(s);
}

function textExercise(size,q,ca,checkFun) {
  // text input area of "size" size, name q, and appropriate onChange()
    var s = "<input type=\"text\" size=\"" + size + "\" name=\"" + q + "\" ";
    var cf = true;
    if (typeof(checkFun) == 'undefined' || checkFun == null) {
        cf = false;
    }
    if (ca == null || ca ) {
        if (!cf) {
            s += "onChange=\"checkAnswer(name,value);\"";
        } else {
            s += "onChange=\"checkAnswer(name,checkFun(value));\"";
        }
    } else if (cf) {
            s += "onChange = \"set???  "; // FIX ME! Placeholder for functional grading
    }
    s += " >";
    return(s);
}

function textProblem(size,q) {  // makes text input area of "size" size, name "q"
    var s = "<input type=\"text\" size=\"" + size + "\" name=\"" + q + "\">";
    return(s);
}

function startExercise(q) {  // writes html to start an exercise, numbered q
    var s;
    if (HI) {
        s = "<strong>Problem " + q.toString() + ".</strong>";
    } else {
        s = "<strong>Exercise " + q.toString() + ".</strong>";
    }
    return(s);
}

function startProblem(q) {  // writes html to start a problem, numbered q
    var s;
    if (HI) {
        s = "<strong>Problem " + q.toString() + ".</strong>";
    } else {
        s = "<strong>Exercise " + q.toString() + ".</strong>";
    }
    return(s);
}

function startSolution(q) {  // html to start a solution, numbered q
    var s;
    if (HI) {
        s = "<strong>Solution to Problem " + q.toString() + ".</strong>";
    } else {
        s = "<strong>Solution to Exercise " + q.toString() + ".</strong>";
    }
    return(s);
}

function writeSelectExercise(mult, q, opt, ans) {
    document.writeln(selectExerciseString(mult, q, opt, ans));
    return(true);
}

function selectExerciseString(mult, q, opt, ans) {
    var name = 'Q' + q.toString();
    var s = selectExercise(mult, name, opt, CA);
    if (CA) {
        if (mult) {
            s += '<input type="button" name="B' + q +
                '" value="Check Answer" onclick="checkAnswer(\'' + name +
                '\',parseMultiple(' + name +'.options))">';
        }
        if (newStyleAnswer != null && newStyleAnswer) {
            boxList[q - 1] = document.images.length;
            s += '<a href="javascript:void(0)" ' +
                ' onClick="giveAnswer(\'Q' + q + '\');">' +
                ' <img src="' + qImgSrc + '" ' +
                ' border="1" align="top"></a>';
        }
    }
    key[q - 1] = ans;
    return(s);
}

function writeTextExercise(size, q, ans) {  // does all the printing for a textfield exercise
    document.writeln(textExercise(size, 'Q' + q, CA));
    var cf = false;
    if (typeof(ans) == 'function') { // TO DO: allow functional checks of answers.
        cf = true;
    }
    if (CA) {
        if (newStyleAnswer != null && newStyleAnswer) {
            boxList[q - 1] = document.images.length;
            document.writeln('<a href="javascript:void(0)" ' +
                ' onMouseOver="status=\'click to see the answer\';return(true)" ' +
                ' onMouseOut="status=defaultStatus;return(true);" ' +
                ' onClick="giveAnswer(\'Q' + q + '\');">' +
                ' <img src="' + qImgSrc + '" ' +
                ' border="1" align="top" alt="click to see the answer"></a>');
        }
    }
    key[q - 1] = ans;
    return(true);
}

function writeRadioExercise(q, opt, ans) {  // write a radio exercise
    document.writeln(radioExercise('Q'+q, opt, CA));
    if (CA) {
        if  (newStyleAnswer != null && newStyleAnswer) {
            boxList[q - 1] = document.images.length;
            document.writeln('<a href="javascript:void(0)" ' +
                ' onClick="giveAnswer(\'Q' + q + '\');">' +
                ' <img src="' + qImgSrc + '" ' +
                ' border="1" align="top"></a>');
        }
    }
    key[q-1] = ans;
    return(true);
}

function radioExercise(q, opt, ca){  // makes a collection of radio inputs.
    var s = "";
    var oplen = opt.length;
    for (var i = 0; i < oplen; i++) {
        s  += '<input type="radio" name="' + q + '" value="' + alphabet[i] + '" ';
        if (ca == null || ca) {
            s += 'onClick="checkAnswer(name,value);"';
        }
        s += '>\n' + alphabet[i] + ') ' + opt[i] + '<br>\n';
    }
    return(s);
}

function selectExercise(mult, q, opt, ca) {
   // makes a select with multiple=mult, name q.
   // if mult, makes the size large enough to show all options.
   // otherwise size=1;  opt is a 1 by array.
    var s;
    var size;
    var oplen = opt.length;
    if (mult) { // leave room for all the answers to be visible.
        size= oplen+1;
    } else {
        size = 1;
    }
    var s = '<select name="' + q + '" size="' + size + '" ';
    if (mult) {
        s += 'multiple="' + mult + '" ';
    }
    if ((ca == null || ca) && !mult) {
        s += 'onChange ="checkAnswer(name,options[selectedIndex].value);"';
    }
    s += '>\n <option>?</option>\n';
    if (oplen <= 26) {
        for (var i=0; i < oplen; i++) {
            s += '<option value="' + alphabet[i] + '">' + ALPHABET[i] +
                ': ' + opt[i] + '</option>\n';
        }
    } else {
        for (var i=0; i < oplen; i++) {
            s += '<option value="' + (i+1).toString() + '">' +
                (i+1).toString() + ': ' + opt[i] + '</option>\n';
        }
    }
    s += '</select>';
    return(s);
}

function scoreProblem(truth,response){
    var answer = parseKey(truth);
    response = trimToLowerCase(response);
    var rsp;
    var correctness;
    if (response == null) {
        correctness = false;
    } else if (type == 'WC') {
        rsp = trimBlanks(response);
        if (rsp.length > 0) {correctness = true};
    } else if (type == 'LA') {
        // try to parse as number; if fail, take literal.
        rsp = evalNum(response);
        if (!isNaN(rsp)) {
            response = rsp;
//        if (response.indexOf('%') != -1) {
//            rsp = parsePercent(response);
//            if (!isNaN(rsp)) {
//                response = rsp;
//            }
//        } else if (!isNaN(parseFloat(trimBlanks(removeCommas(response))))) {
//            response = parseFloat(trimBlanks(removeCommas(response)));
        }
        if (answer.toString() == response.toString()) {
            correctness = true;
        } else {
            correctness = false;
        }
    } else if (type == 'RG') {
        var r = evalNum(response);
        if ((answer[0] <= r) && (r <= answer[1])) {
            correctness = true;
        } else {correctness = false;}
    } else if (type == 'MA') {
        correctness = false;
        for (var i=0; i < answer.length; i++) {
            if (response == answer[i]) {
                correctness = true;
            }
        }
    } else if (type == 'MR') {
        correctness = false;
        resArray = response.split(',');
        resArray.sort()
        if (resArray.length == answer.length) {
            correctness = true;
            for (var i=0; i < answer.length; i++ ) {
                if (answer[i] != trimToLowerCase(resArray[i])) {
                    correctness = false;
                }
            }
        }
    }
    return(correctness);
}

function parseKey(s) {
// parses the answer keys for interactive grading.  See header.
    s = trimBlanks(s.toLowerCase()) // use only lower-case letters
    var answer;
    if (s.indexOf(':') != -1) {          // solution is a range
        answer = s.split(':')
        if (answer.length != 2) { alert('Error #1 in irGrade.parseKey(): bad range syntax!') }
        type = 'RG'             // answer is of type range (RG)
        for (var i=0; i < answer.length; i++) {
            answer[i] = parsePercent(answer[i]);
            if (isNaN(answer[i])){
                alert('Error #2 in irGrade.parseKey(): unparsable number in range!');
            }
        }
        ansText = answer[0] + ' to ' + answer[1];
    } else if (s.indexOf('&') != -1 ){ // multiple required answers; assume all
                                       // are letters
        answer = s.split('&');
        type = 'MR'                    // answer is of type multiple required (MR)
        for (var i=0; i < answer.length; i++ ) {
            answer[i] = trimBlanks(answer[i].toLowerCase());
            answer.sort();
        }
        ansText = answer[0];
        for (var i=1; i < answer.length; i++) {
            ansText += ' and ' + answer[i];
        }
    } else if (s.indexOf('|') != -1 ){ // multiple answers accepted; assume all
                                       // are letters
        answer = s.split('|');
        type = 'MA';                   // answer is of type multiple accepted (MA)
        for (var i=0; i < answer.length; i++ ) {
            answer[i] = trimBlanks(answer[i].toLowerCase());
        }
        ansText = answer[0];
        for (var i=1; i < answer.length; i++) {
            ansText += ' or ' + answer[i];
        }
    } else if (s == '*') {
        type = 'WC';                   // wildcard
        answer = '*';
        ansText = 'any non-blank answer';
    } else {                           // answer is literal
        type = 'LA'                    // literal answer (LA)
        answer = parsePercent(s);
        if (isNaN(answer)) {
            answer = trimBlanks(s.toLowerCase());
        }
        ansText = answer;
    }
    return(answer);
}

function setCourse(inx) {
    setCourseSpecs(inx);
    document.applets[0].getSidFile(courses[inx][1] + sFileBase);
    return(document.applets[0].setBox(courses[inx][1] + dFileBase));
}

function setCourseSpecs(inx) {
    course = courses[inx][1];
    courseName = courses[inx][2];
    teacher = courses[inx][3];
    teacherName = courses[inx][4];
    gPath = courses[inx][5];
    dFile = cRoot + course + dFileBase;
    var sFile = cRoot + course + sFileBase;
}

function setDFile(inx) {
    setCourseSpecs(inx);
    document.forms[0].elements["dFile"].value = dFile;
    document.forms[0].elements["class"].value = courses[inx][1];
    return(true);
}

function writeCourseOptions(fun) {
    document.writeln('<font color="green">Course:</font><select name="course" ' +
                     'onchange="' + fun + '(options[selectedIndex].value);">');
    for (var i=0; i < courses.length; i++ ) {
        document.writeln('<option value="' + (courses[i][0]).toString() + '">' +
            (courses[i][2]).toString() + '</option>');
    }
    document.writeln('</select>');
    return(true);
}

function spawnGradePage(theForm) {
    if (validateLablet(theForm)) {
        var scorQStr = scoreBase + "class=" + course + "&gpath=" + gPath +
                        "&teacher=" + teacher;
        var queryURL = queryBase + "class=" + course + "&gpath=" + gPath +
                        "&teacher=" + teacher + "&sid=" + theForm.sid.value +
                        "&passwd=" + escape(theForm.sid.value) + "&dFile=" + dFile +
                        "&querylist=" + queryList;
        var histURL  = '../Grades/scoreHist.htm';
        gradePage = open('',"gradePage",'toolbar=yes,location=no,directories=no,status=yes,'+
            'scrollbars=yes,resizable=yes');
        gradePage.document.open();
        gradePage.scorQStr = scorQStr;
        gradePage.teacherName = teacherName;
        gradePage.courseName = courseName;
        gradePage.semester = semester;
        gradePage.document.writeln("<html><head><title>SticiGui Grade Query</title></head>");
        gradePage.document.writeln("<frameset rows=\"*,300\">");
        gradePage.document.writeln("<frame name=\"queryWin\" src=\"" + queryURL + "\"" +
            " frameborder=\"1\" framespacing=\"0\" border=\"1\">");
        gradePage.document.writeln("<frame name=\"histWin\" src=\"" + histURL + "\"" +
            " frameborder=\"1\" framespacing=\"0\" border=\"1\">");
        gradePage.document.writeln("</frameset></html>");
        gradePage.document.close();
        return(true);
    } else {
        return(false);
    }
}

function spawnProblem(theForm,i) {
    if (validateLablet(theForm)) {
        var ck = document.cookie;
        var fname = formStemName + i.toString();
        var assigned = document.applets[0].isAssigned(fname);
        if (!assigned) {
                    alert('This problem set is not yet assigned.\n' +
                            'Try again later.');
                    return(false);
        }
        var sstr =  crypt("sid" + theForm.sid.value, theForm.sid.value) + '=';
        if (ck.indexOf(sstr) < 0){
            var rs = (theForm.sid.value).toString();
            if (rs.length < 10){ 
                 rs += rs;
            }
            randSeed = parseInt(rs.substr(0,Math.min(10,rs.length)));
            setSubmitCookie("sid", theForm, true);
            ck = document.cookie;
            if (ck.indexOf(sstr) < 0){
                alert('Error #1 in irGrade.spawnProblem()!\n' +
                      'Make sure your browser is configured to accept cookies.\n' +
                      'Clear existing cookies and try again.');
                return(false);
            }
        }
        var ss = ck.substring(ck.indexOf(sstr) + sstr.length, ck.length);
        if (ss.indexOf(';') > -1) {ss = ss.substring(0,ss.indexOf(';'))};
        var cl = crypt(ss, theForm.sid.value);
        var instr = '../Problems/set' + i.toString() + 'i.htm';
        var appl  = '../Problems/set' + i.toString() + 'j.htm';
        lablet = open('',"lablet",'toolbar=no,location=no,directories=no,status=no,'+
            'scrollbars=yes,resizable=yes');
        lablet.document.open();
        lablet.continueLab = cl;
        var reveal = document.applets[0].revealKey(fname);
        var allowSubmit = document.applets[0].allowSubmit(fname);
        lablet.reveal = reveal;
        lablet.allowSubmit = allowSubmit;
        lablet.dFile = dFile;
        lablet.course = course;
        lablet.teacher = teacher;
        lablet.document.writeln("<html><head><title>SticiGui Problem Set " + i
                +"</title></head>");
        lablet.document.writeln("<frameset rows=\"*,300\">");
        lablet.document.writeln("<frame name=\"instrWin\" src=\"" + instr + "\"" +
            " frameborder=\"1\" framespacing=\"0\" border=\"1\">");
        lablet.document.writeln("<frame name=\"appletWin\" src=\"" + appl + "\"" +
            " frameborder=\"1\" framespacing=\"0\" border=\"1\">");
        lablet.document.writeln("</frameset></html>");
        lablet.document.close();
        return(true);
    } else {
        return(false);
    }
}

function checkAnswer(number, response) {
  // check response against key[number-1].  If number is not an integer,
  // calls findNum(number) to remove characters to try to leave an integer.
    var theQuestion = findNum(number);
    var truth = scoreProblem(key[theQuestion-1],response);
    if (truth) {
        document.images[boxList[theQuestion-1]].src = rightImgSrc;
    } else {
        document.images[boxList[theQuestion-1]].src = wrongImgSrc;
    }
    return(truth);
}

function isAnswered(qVal) { // checks whether the student answered a question
    var n = findNum(qVal) - 1;
    var iA = false;
    var inx;
    var el = document.forms[0].elements[qVal];
    var qT = el.type;
    if (qT == 'select-one') {
        inx = el.selectedIndex;
        if (inx > 0) {
            iA = true;
        }
    } else if (qT == 'select-multiple') {
        resp = parseMultiple(el.options);
        if (resp != null && resp != '' && resp != '?') {
            iA = true;
        }
    } else if (qT == 'text') {
        resp = el.value;
        if (resp != null && resp != '' && removeAllBlanks(resp) != null &&
                 removeAllBlanks(resp).length > 0) {
            iA = true;
        }
    } else if (qT == 'radio') { // incomprehensible bugs with this
        for (var i=0; i < el.length; i++) {
            if (el[i].checked) {
                iA = true;
            }
        }
    } else if ( typeof(qT) == 'undefined' || qT == null ) { // assume it is a radio
        for (var i=0; i < el.length; i++) {
            if (el[i].checked) {
                iA = true;
            }
        }
    } else {
        alert('Error #1 in irGrade.js.isAnswered(): input type ' + qT +
           ' is not supported!');
    }
    return(iA);
}


function giveAnswer(number) {
 // display the answer to question[number] in a new window, provided the student has
 // tried to answer the question
    answer = parseKey(key[findNum(number)-1]);
    ansWin = window.open('','ansWin',
        'toolbar=no,location=no,directories=no,status=no,' +
        'scrollbars=yes,resizable=yes,width=230,height=160');
    ansWin.document.write(startHtm);
    if (isAnswered(number)) {
        ansWin.document.write('<p align="center"><b>The correct answer is <br> \n' +
           '<font color="green">' + ansText + '</font></b>.</p>\n' + dsmsHtm + endHtm);
    } else {
        ansWin.document.write('<p align="center"><b>You must answer before you can ' +
                   'see the solution.</b>' + dsmsHtm + endHtm);
    }
    return(true);
}

function validEmail(e) { // checks whether e appears to be a valid email address
    var okEmailChars="._-@:%0123456789abcdefghijklmnopqrstuvwxyz";
    var truth = true;
    if (e == null || e.length == 0) {
        truth=false;
    } else {
        et = trimToLowerCase(e);
        if (et.indexOf('@') == -1 ||
            ( (et.lastIndexOf('.') != et.length - 4 ) &&
              (et.lastIndexOf('.') != et.length - 3 ) ) )
            truth = false;
        else if (et.indexOf('@') != et.lastIndexOf('@')) {
            truth = false;
        } else {
            for (var i=0; i < et.length; i++) {
                if(okEmailChars.indexOf(et.charAt(i)) < 0) {truth = false;}
            }
        }
    }
    return(truth);
}

function validSID(s){ // check whether SID is valid
    var digits="0123456789";
    var truth = false;
    if (s.length == 8 && s.charAt(0) == "1" || s.length == 9) {
        truth = true;
        for (var i=0; i < s.length; i++){
            if (digits.indexOf(s.charAt(i)) < 0) {truth = false;}
        }
    }
    return(truth);
}

function validateLabletSubmit(theForm){
// check that various form entries are filled in correctly, submit or cancel
    if (validateLablet(theForm)){
        return(labletSubmit(theForm));
    } else {
        return(false);
    }
}

function labletSubmit(theForm) {
    theForm.submitTime = new Date();
    confirmStr = 'Your assignment is ready to submit, ' +
        theForm.firstName.value + ' ' +
        theForm.lastName.value +
        '.\nPress \"OK\" to submit it now, or \"Cancel\" to return to the assignment.';
    if (confirm(confirmStr)){
        setExtraInputs(theForm);
        setSubmitCookie(setNum.toString(),theForm,false);
        document.forms[1].action = "http://www.stat.berkeley.edu/cgi-bin/grader";
        var s = collectResponses(theForm,true,true);
        document.forms[1].elements['contents'].value = crypt(s,bigPi);
        document.forms[1].submit();
        return(true);
    } else {
        alert("Your assignment has NOT been submitted.");
        return(false);
    }
}

function validateLablet(theForm) {
    if (theForm.lastName.value == null || theForm.lastName.value.length == 0 ||
             allBlanks(theForm.lastName.value) ) {
        alert('Last Name is missing');
        theForm.lastName.focus();
        return(false);
    } else if (!allLetters(theForm.lastName.value) ) {
        alert('Illegal character(s) in Last Name');
        theForm.lastName.focus();
        return(false);
    } else if (theForm.firstName.value == null ||
          theForm.firstName.value.length == 0 || allBlanks(theForm.firstName.value)) {
        alert('First Name is missing');
        theForm.firstName.focus();
        return(false);
    } else if (!allLetters(theForm.firstName.value) ) {
        alert('Illegal character(s) in First Name');
        theForm.firstName.focus();
        return(false);
    } else if ( !validEmail(theForm.email.value)) {
        alert('Email address is missing or invalid');
        theForm.email.focus();
        return(false);
    } else if (!document.applets[0].acceptSid(theForm.sid.value)) {
        alert('This SID is not enrolled.\nDid you set the course correctly?');
        theForm.sid.focus();
        return(false);
    } else if (!validSID(theForm.sid.value)) {
        alert('Invalid password');
        theForm.sid.focus();
        return(false);
    } else if (!document.applets[0].isOkPasswd(trimBlanks(theForm.sid.value),
             trimBlanks(theForm.email.value))) {
        alert('This email address is not enrolled, or does not match the SID.\n' +
              'Did you set the course correctly?');
        theForm.email.focus();
        return(false);
    } else {
        theForm.lastName.value = trimBlanks(theForm.lastName.value);
        theForm.firstName.value = trimBlanks(theForm.firstName.value);
        theForm.email.value = trimBlanks(theForm.email.value);
        theForm.sid.value = trimBlanks(theForm.sid.value);
        return(true);
    }
}

function saveResponses(fname,theForm,saveAns) {
    if(setSubmitCookie(fname,theForm,false)) {
        confirm('Your answers have been saved as a cookie on your computer.\n' +
               'Cookies are NOT RELIABLE storage--do not count on them!\n' +
               'You should write your answers down, too.\n' +
               'The cookie will be erased automatically in ' + 
               cookieExpireDays.toString() + ' days.');
        return(true);
    } else {
        alert('Error #1 in irGrade.saveResponses:\nYour answers have NOT been saved!\n' +
                'Something went wrong.');
        return(false);
    }
}

function setSubmitCookie(fff,theForm,idInfo){
    var s = collectResponses(theForm,false,idInfo);
    document.cookie = crypt(fff + theForm.sid.value, theForm.sid.value) +
          "=" + crypt(s, theForm.sid.value) + ";EXPIRES=" + expireTimeString();
    return(true);
}

function recoverResponses() {
    if (continueLab == null) {
        return(false);
    } else {
        var theSid = getCookieVal(continueLab,"sid");
        var thePw = theSid;
        var tv = false;
        var theForm = document.forms[0];
        var ascStr = crypt(setNum.toString() + theSid, thePw)+ "=";
        var searchStr = document.cookie;
        var startInx = searchStr.indexOf(ascStr);
        if (startInx < 0 ) {
            return(false);
        }
        searchStr = searchStr.substring(startInx+ascStr.length,searchStr.length);
        var endInx = searchStr.indexOf(";");
        if (endInx > -1 ) {
            searchStr = searchStr.substring(0,endInx);
        }
        searchStr = crypt(searchStr,thePw) + "&";
        var qName;
        var aText;
        var inx;
        var ampInx;
        var elem;
        for (var i=0; i < theForm.elements.length; i++) {
            qName = theForm.elements[i].name + '=';
            if (qName.indexOf("Q") == 0) {
                elem = theForm.elements[i];
                if (elem.type == "select-multiple") {
                    while (searchStr.indexOf(qName) > -1) {
                           inx = searchStr.indexOf(qName);
                           searchStr = searchStr.substring(inx+qName.length,searchStr.length);
                           ampInx = searchStr.indexOf('&');
                           qText = unescape(searchStr.substring(0,ampInx));
                           // search for option to select
                           var ag = false;
                           for (var j=0; j < elem.length; j++) {
                                if (elem[j].value == qText) {
                                    elem.options[j].selected = true;
                                    ag = true;
                                }
                           }
                    }
                    // if (CA && ag){
                        //tv = checkAnswer(qName.substr(0,qName.length - 1),
                        //  parseMultiple(theForm.elements[i].options));
                    //}
                } else if (elem.type == 'select-one') {
                       inx = searchStr.indexOf(qName);
                       if (inx > -1){
                           searchStr = searchStr.substring(inx+qName.length,searchStr.length);
                           ampInx = searchStr.indexOf('&');
                           qText = unescape(searchStr.substring(0,ampInx));
                           // search for option to select
                           for (var j=0; j < elem.length; j++) {
                             if (elem[j].value == qText) {
                                elem.options[j].selected = true;
                             }
                           }
                    }
                    // if (CA) {
                    //  var si = theForm.elements[i].options.selectedIndex;
                    //  if (si != null) {
                    //      tv = checkAnswer(qName.substr(0,qName.length - 1),
                    //      elem.options[si].value);
                    //  }
                    //}
                } else if (elem.type == 'text') {
                       inx = searchStr.indexOf(qName);
                       if (inx > -1) {
                           searchStr = searchStr.substring(
                                inx+qName.length,searchStr.length);
                           ampInx = searchStr.indexOf('&');
                           qText = unescape(searchStr.substring(0,ampInx));
                           elem.value = qText;
                       }
                       // if (CA) {
                       //       var dba = trimBlanks(elem.value);
                        //  if (dba.length > 0) {
                     //             tv = checkAnswer(qName.substr(0,qName.length - 1),
                     //                              elem.value);
                        //  }
                        //}
                } else if (elem.type == null || elem.type == 'radio' || 
                           elem.type == 'undefined') {
                    inx = searchStr.indexOf(qName);
                       if (inx > -1){
                           searchStr = searchStr.substring(inx+qName.length,searchStr.length);
                           ampInx = searchStr.indexOf('&');
                           qText = unescape(searchStr.substring(0,ampInx));
                           if (elem.value == qText) {
                                elem.checked = true;
                           }
                       }
                } else {
                    alert('Error #1 in irGrade.recoverResponses(): unsupported problem type ' +
                        elem.type + '!');
                    return(false);
                }
            }
        }
    }
    return(true);
}

function collectResponses(theForm,saveAs,saveId) {
    var typ;
    var nam;
    var s = '';
    s += 'randSeed=' + escape(randSeed) + '&';
    for (var i=0; i < theForm.elements.length; i++) {
        typ = theForm.elements[i].type;
        nam = theForm.elements[i].name;
        if (typ == "button" || typ == "submit" || typ == "reset") {
        } else if (!saveId && (nam == "lastName" || nam == "firstName" ||
                             nam == "sid" || nam == "sid2" || nam == "email" ||
                             nam == "passwd" || nam == "passwd2")) {
        } else if (typ == "select-one") {
            s += escape(nam) + "=" +
                 escape(theForm.elements[i].options[
                   theForm.elements[i].options.selectedIndex].value)+ "&";
        } else if (typ == "select-multiple") {
            for (var j=0; j < theForm.elements[i].options.length; j++) {
                if (theForm.elements[i].options[j].selected) {
                    s += escape(nam) + "=" +
                        escape(theForm.elements[i].options[j].value) + "&";
                }
            }
        } else if (typ == "radio") {
            if (theForm.elements[i].checked) {
                s += escape(nam) + '=' +
                    escape(theForm.elements[i].value) + '&';
            }
        } else {
            s += escape(nam) + "=" + escape(theForm.elements[i].value) + "&";
        }
    }
    if (saveAs) {
        for (var i=0; i < key.length; i++) {
            s += escape('a' + (i+1).toString()) + '=' + escape(key[i].toString()) + '&';
        }
    }
    if (s[s.length-1] == "&") {
        s = s.substring(0,s.length - 1);
    }
    return(s);
}

function labSetUp(seed, sn) {
    isLab = true;
    sectionContext = 'function setSectionContext() { \n' 
    window.name= 'seti';
    setNum = sn;
    HI = true;
    if (typeof(parent.reveal) == 'undefined') {
        CA = 'invalid';
    } else {
        CA = parent.reveal;
    }
    if (seed != "SeEd") {
        rand = new rng(parseInt(seed));
    } else {
        continueLab = parent.continueLab;
        if (continueLab == null || CA == 'invalid') {
            alert('Error #1 in irGrade.labSetUp()!\n' +
                'Problem Set not initialized correctly.\n' +
                'You must use the Problem Set Form to go to this page.\n' +
                'If you did, make sure your browser is configured to accept cookies, ' +
                ' and try again.\n ');
            document.close();
            window.close();
            return(false);
        } else {
            var searchStr = continueLab + '&';
            var pat = "randSeed=";
            var inx = searchStr.indexOf(pat);
            if (inx < 0) {
                alert('Error #2 in irGrade.labSetUp()!\n' +
                    'Problem Set not recovered correctly.\n' +
                    'Questions may have changed!');
                rand = new rng();
            } else {
                searchStr = searchStr.substring(
                    inx + pat.length, searchStr.length);
                randSeed = unescape(searchStr.substring(0,searchStr.indexOf('&')));
                rand = new rng(randSeed);
            }
        }
    }
    randSeed = rand.getSeed();
    writeProblemSetHeader(setNum);
    return(true);
}

function setRequiredInputs(theForm) {
    theForm.elements['lastName'].value =  getCookieVal(continueLab,"lastName");
    theForm.elements['firstName'].value = getCookieVal(continueLab,"firstName");
    theForm.elements['email'].value = getCookieVal(continueLab,"email");
    theForm.elements['sid'].value = getCookieVal(continueLab,"sid");
    theForm.elements['sid2'].value = getCookieVal(continueLab,"sid");
    theForm.elements['passwd'].value = getCookieVal(continueLab,"sid");
    theForm.elements['passwd2'].value = getCookieVal(continueLab,"sid");
    return(true);
}

function setExtraInputs(theForm) {
    theForm.elements['inlinekey'].value = inlinePrefix + (qCtr-1).toString();
    theForm.elements['dFile'].value = parent.dFile;
    theForm.elements['teacher'].value = parent.teacher;
    theForm.elements['class'].value = parent.course;
    theForm.elements['extrainfo'].value = escape("seed=" + randSeed.toString() +
        "&irGradeVersion=" + irGradeModTime.toString());
    var nRight = 0;
    var qVal;
    var resp;
    var qType;
    for (var i=1; i < qCtr; i++) {
        qVal = 'Q' + i.toString();
        qType = theForm.elements[qVal].type;
        if (qType == 'select-one') {
            resp = theForm.elements[qVal].options[
               theForm.elements[qVal].options.selectedIndex].value;
        } else if (qType == 'select-multiple') {
            resp = parseMultiple(theForm.elements[qVal].options);
        } else if (qType == 'text') {
            resp = theForm.elements[qVal].value;
        } else if (qType == 'radio') {
            if (theForm.elements[qVal].checked) {
                resp = theForm.elements[qVal].value;
            }
        } else if (qType == null || qType == 'undefined') { // radio too!
            if (theForm.elements[qVal].checked) {
                resp = theForm.elements[qVal].value;
            }
        } else {
            alert('Error #1 in irGrade.setExtraInputs(): Input type ' + qType +
               ' in question ' + qVal + ' is not supported!');
        }
        if (scoreProblem(key[i-1],resp)) nRight++;
    }
    theForm.elements['score'].value = roundToDig(100*nRight/(qCtr - 1),2).toString();
    return(true);
}

function killApplets() { // dispose of applets when leaving the page
    for (var i = 0; i < document.applets.length; i++) {
        document.applets[i].stop();
        document.applets[i].destroy();
    }
    return(true);
}
    

// ============================================================================
// ============================ SPECIAL HTML GENERATORS =======================

function chapterSetUp(seed, chNum, titStr) {
    theChapter = null;
    for (var i=0; i < theChapters.length; i++) {
        if (theChapters[i].toString() == chNum.toString()) {
            theChapter = i;
        }
    }
    if (theChapter == null) {
        alert('Error in irGrade.chapterSetUp(): chapter ' + chNum +
              ' is not in the index!');
        theChapter = 0;
    }
    var statStr;
    if (seed != "SeEd") {
        rand = new rng(parseInt(seed));
    } else {
        rand = new rng();
    }
    window.name = "bookWin";
    if (typeof(titStr) == 'undefined' || titStr == null ) {
        statStr = "Chapter " + (theChapters[theChapter]).toString();
    } else {
        statStr = titStr;
    }
    defaultStatus = "SticiGui " + statStr;
    document.writeln('<head><title>' + defaultStatus + '</title>' +
                     '<base target="glossWin"></head>');
    document.writeln("<h1 align=\"center\"><a href=\"../index.htm\" " +
            " target=\"_top\">SticiGui</a> " + statStr + "</h1>");
    CA = (1==1);
    HI = false;
    randSeed = rand.getSeed();
    sectionContext = 'function setSectionContext() { \n' 
    return(true);
}

function examSetUp(seed, sName, sn) {
    isLab = false;
    sectionContext = 'function setSectionContext() { \n' 
    window.name= 'seti';
    examName = sName;
    examNum = sn;
    HI = true;
    if (seed != "SeEd") {
        rand = new rng(parseInt(seed));
    } else {
        rand = new rng();
    }
    randSeed = rand.getSeed();
    writeExamHeader(examName, examNum.toString() + '.' + randSeed.toString() );
    defaultStatus = 'SticiGui ' + examName + ' ' + examNum.toString();
    return(true);
}

function writeExamHeader(exNam, exVer) {
    document.writeln("<form name=\"labletForm\" method=\"POST\">");
    document.writeln("<h1 align=\"center\"><a href=\"../index.htm\" target=\"_new\">" +
        "SticiGui</a> " + exNam + '</h1><h3 align="center"> Version ' + 
        exVer.toString() + '</h3>');
    return(true);
}

function setApplets() {
    if (  (typeof (document.forms) != 'undefined') && (document.forms != null) &&
            (document.forms.length > 0 ) && !isLab )  {
        sectionContext += 'document.forms[0].reset();\n';
    }
    if (isLab) {
        sectionContext += 'recoverResponses();\n';
    }
    sectionContext += '}';
    eval(sectionContext);
    setSectionContext();
}

function writeProblemSetFooter() {
    document.writeln('<div align="center"><center><table ' +
                        'border="0" cellspacing="1"><tr><td>');
    if (parent.allowSubmit) {
        document.writeln('<input type="button" name="subBut" ' +
            ' value="Submit for Grading" ' +
            ' onClick="labletSubmit(this.form);">');
        document.writeln('<input type="button" name="saveBut" value="Save Answers" ' +
        ' onClick="saveResponses(setNum.toString(),this.form,false)"> ');
    } else {
        document.writeln(' <input type="reset" name="reset" value="Clear Form"> ');
    }
    document.writeln('</td></tr></table></center></div><p>&nbsp;</p> ');
    document.writeln('</form><form method="POST"><input type="hidden" name="contents"></form>');
    printFootnotes();
    writeMiscFooter(false);
    return(true);
}

function writeSolution(p,text,evalStr) {
    longKey[p-1] = text;
    solutionCode[p-1] = evalStr;
    document.writeln('<br><a href="#solution_' + p.toString() +
        '" target="_self">Solution</a>.');
    return(true);
}

function writeFootnote(p,label,text) {
    footnotes[p] = text;
    document.writeln('<sup><a href="#footnote_' + p.toString() +
        '" target="_self">' + label.toString() + '</a></sup>');
    return(true);
}

function printFootnotes() {
    if (footnotes.length > 0) {
        document.writeln('<hr size="3" width="70%" align="center"><h3 align="center">' +
            'Footnotes</h3>');
        for (var i=0; i < footnotes.length; i++ ) {
            if (typeof(footnotes[i]) != 'undefined') {
               document.writeln('<p><a name="footnote_' + i.toString() + '"></a>' +
                  '<strong>Footnote ' + (i+1).toString() + '.</strong> ' +
                  footnotes[i] + '</p>');
            }
        }
    }
}

function writeChapterFooter() {
    printFootnotes();
    if (longKey.length > 0 ) {
        document.writeln('<hr size="3" width="70%" align="center"><h3 align="center">' +
            'Solutions to Selected Exercises</h3>');
        for (var i=0; i < longKey.length; i++ ) {
            if (typeof(longKey[i]) != 'undefined') {
               document.writeln('<p><a name="solution_' + (i+1).toString() + '"></a>' +
                  '<strong>Solution to exercise ' + (i+1).toString() + '.</strong> ' +
                  longKey[i] + '</p>');
            }
            if (typeof(solutionCode[i]) != 'undefined' && solutionCode[i] != null
                               && solutionCode[i] != "") {
                eval(solutionCode[i]);
            }
        }
    }
    document.writeln('<hr size="3" width="70%" align="center"><h3 align="center">');
    if (theChapter > 1) {
        document.writeln('<a href="ch' + (theChapters[theChapter-1]).toString() + '.htm" ' +
            'target="_self" onmouseout="window.status=defaultStatus;return(true);" ' +
            'onmouseover="window.status=\'Chapter ' + (theChapters[theChapter-1]).toString() +
            '\';return(true);">Chapter ' + (theChapters[theChapter-1]).toString() + 
            '</a> <small>(previous)</small>&nbsp;&nbsp;|&nbsp;&nbsp; ') ;
    }
    document.writeln('Chapter ' + theChapters[theChapter] + ' <small>(current)</small> ');
    if (theChapter < theChapters.length - 1) {
        document.writeln('&nbsp;&nbsp;|&nbsp;&nbsp;' +
            '<a href="ch' + (theChapters[theChapter+1]).toString() + '.htm" ' +
            'target="_self"  onmouseout="window.status=defaultStatus;return(true);" ' +
            'onmouseover="window.status=\'Chapter ' + (theChapters[theChapter+1]).toString() +
            '\';return(true);">Chapter ' + (theChapters[theChapter+1]).toString() + 
            '</a> <small>(next)</small><br> ') ;
    }
    document.writeln('<a href="toc.htm" target="_self" ' +
            'onmouseout="window.status=defaultStatus;return(true);" ' +
            'onmouseover="window.status=\'Table of Contents\';return(true);">' +
            'Text Table of Contents</a><br> ' +
            '<a href="../Problems/index.htm" target="_self" ' +
            'onmouseout="window.status=defaultStatus;return(true);" ' +
            'onmouseover="window.status=\'Computer-graded assignments\';return(true);">' +
            'Problem Sets</a><br>' +
            '<a href="gloss.htm" onmouseout="window.status=defaultStatus;return(true);" ' +
            'onmouseover="window.status=\'Extensive statistical glossary\';return(true);">' +
            'Glossary</a><br> ' +
            '<a href="../index.htm" target="_top" ' +
            'onmouseout="window.status=defaultStatus;return(true);" ' +
            'onmouseover="window.status=\'SticiGui Home\';return(true);">' +
            'SticiGui Home</a> </h3> ');
    writeMiscFooter(false);
    return(true);
}

function writeMiscFooter(sr) {
    var stra = '<h3 align="center"><a href="';
    var strb = '/index.htm" target="_top" onmouseout="window.status=defaultStatus;return(true);" ' +
               ' onmouseover="window.status=\'SticiGui Home\';return(true);">' +
               'SticiGui Home</a></h3>';
    if (typeof(sr) == 'boolean') {
        if (sr) {
           document.writeln(stra + '..' + strb);
        }
    } else {
        document.writeln(stra + sr + strb);
    }
    document.write('<p><font color="#FF0000"><small>&copy;' + copyYr + pbsRef +
        '. All rights reserved.</small></font><br><small>Last generated ' + today +
        '. ');
    if ( !(typeof(pageModDate) == 'undefined') && !(pageModDate == null) ) {
        document.write('Content last modified ' + pageModDate + '. ');
    }
    document.writeln('</small></p>');
    return(true);
}

function writeProblemSetHeader(sn) {
    document.writeln("<form name=\"labletForm\" method=\"POST\">");
    var formName = "SticiGuiSet" + sn.toString();
    document.writeln("<input type=\"hidden\" name=\"formname\" value=\"" +
                formName + "\">");
    continueLab = parent.continueLab;
    if (continueLab == null) {
        alert('Error #1 in irGrade.writeProblemSetHeader()!\n' +
            'The problem set is not correctly initialized.\n' +
            'Make sure your browser is set up to accept cookies and try again.');
        document.close();
        window.close();
        return(false);
    } else {
        document.writeln("<input type=\"hidden\" name=\"lastName\" value=\" \">" +
            " <input type=\"hidden\" name=\"firstName\" value=\" \">" +
            " <input type=\"hidden\" name=\"email\" value=\" \">" +
            " <input type=\"hidden\" name=\"sid\" value=\" \">" +
            " <input type=\"hidden\" name=\"sid2\" value=\" \">" +
            " <input type=\"hidden\" name=\"passwd\" value=\" \">" +
            " <input type=\"hidden\" name=\"passwd2\" value=\" \">");
    }
    document.writeln("<input type=\"hidden\" name=\"inlinekey\"" +
        "value=\" \">");
    document.writeln("<input type=\"hidden\" name=\"dFile\"" +
        " value=\" \">");
    document.writeln("<input type=\"hidden\" name=\"teacher\" value=\" \">");
    document.writeln("<input type=\"hidden\" name=\"class\" value=\" \">");
    document.writeln("<input type=\"hidden\" name=\"score\" value=\" \">");
    document.writeln("<input type=\"hidden\" name=\"extrainfo\" value=\" \">");
    setRequiredInputs(document.forms[0]);
    document.writeln("<h1 align=\"center\"><a href=\"../index.htm\" target=\"_new\">" +
        "SticiGui</a> Problem Set "
        + sn.toString() + "</h1>");
    return(true);
}

function makeOptions(ans, pert, dig, extra) { // make a set of numerical options as answers,
                                              // converted to strings with dig digits of
                                              // precision. each answer is perturbed by a
                                              // signed multiple of pert.
                                              // extra is appended to each answer
    if (typeof(dig) == "undefined" || dig == null) {
        dig = 0;
    }
    if (typeof(extra) == "undefined" || extra == null) {
        extra = "";
    }
    var rs = listOfRandSigns(5);
    var rawOpt = new Array(5);
    for (var i=0; i < 5; i++) {
        rawOpt[i] = commify(roundToDig(ans + i*rs[i]*pert,dig)) + extra;
    }
    var optPerm = randPermutation(rawOpt,"inverse");
    optPerm[1] = alphabet[optPerm[1][0]];
    return(optPerm);
}

function makeRangeOptions(t, lo, hi, loLim, hiLim, dig, extra, iterLim) { 
             // make multiple choice options
             // t is truth, lo is starting lower limit, hi is starting upper limit,
             // loLim is ultimate lower limit, hiLim is ultimate upper limit,
             // dig is digits of precision, extra is appended to each  answer
             // steps up and down by 10% until t is within 0.3 of the space between
             // answers of one of the answers
    var pertFac = 0.07;      // amount by which to move endpoints each iteration
    var closeFac = 0.35;     // how much closer should answer be to one endpoint?
    var av;
    var altern = false;      // alternate moving upper and lower endpoints
    if (typeof(extra) == 'undefined' || extra == null) {
        extra = "";
    }
    if (typeof(dig) == 'undefined' || dig == null) {
        dig = 2;
    }
    var ok = false;
    var lim = 0;
    var maxIt;
    if (typeof(iterLim) == 'undefined' || iterLim == null) {
        maxIt = maxIterations;
    } else {
        maxIt = iterLim;
    }
    while(!ok && lim <= maxIt) {
       var ans = linspace(lo,hi,5);
       var d = closeFac*(ans[1]-ans[0]);    // want to be significantly closer to one end
       if (t <= ans[0] ) {
           av = alphabet[0];
           ok = true;
       } else if ( t >= ans[4]) {
           av = alphabet[4];
           ok = true;
       } else {
           for (i=0; i < ans.length; i++) {
              if (Math.abs(t - ans[i]) <= d) {
                 ok = true;
                 av = alphabet[i];
              }
            }
            if (altern) {
                lo = loLim + (1.0 - pertFac)*(lo-loLim);
                altern = !altern;
            } else {
                hi = hi + pertFac*(hiLim-hi);
                altern = !altern;
            }
        }
        if (lim++ == maxIt) {
            av = alphabet[0];
            var ref = Math.abs(t-ans[0]);
            for (var i=1; i < ans.length; i++ ) {
                var nref = Math.abs(t-ans[i]);
                if (nref < ref) {
                    ref = nref;
                    av = alphabet[i];
                }
            }
            ok = true;
            alert('Error #1 in irGrade.makeRangeOptions: maximum iterations exceeded in ' +
                  'problem ' + (pCtr-1).toString() + '.\nPlease report this to your ' +
                  'instructor.');
        }
    }
    var opt = new Array(ans.length);
    for (i=0; i < ans.length; i++) {
       opt[i] = (roundToDig(ans[i],dig)).toString()+ extra;
    }
    var out = new Array(2);
    out[0] = opt;
    out[1] = av;
    return(out);
}

function makeProbOptions(t, lo, hi, dig, iter) {
    if (typeof(dig) == 'undefined' || dig == null) {
        dig = 0;
    }
    return(makeRangeOptions(100*t,100*lo,100*hi,0,100,dig,'%', iter));
}

function truthTable(title, valArr) { // make a 2 by 2 truth table
    if (valArr.length < 4) {
        alert('Error #1 in irGrade.truthTable: number of truth values is ' + valArr.length);
        return(null);
    } else {
        return(truthTableHeader(title) +
            valArr[0] + '</td><td align="center">' +
            valArr[1] +
            '</td></tr><tr><td align="right" bgcolor="lightblue">F</td><td></td>' +
            '<td align="center">' +
            valArr[2] + '</td><td align="center">' +
            valArr[3] + '</td><tr></table></center></div>'
        );
     }
}

function writeTruthTable(title, valArr) {
    document.writeln(truthTable(title, valArr));
    return(true);
}

function truthTableHeader(title) {
    return( '<div align="center"><center><table border="2" cellspacing="2">' +
            '<tr><td colspan="4" align="center" bgcolor="lightblue">Truth table for ' +
            title + '</td></tr><tr><td></td>' +
            '<td align="right" bgcolor="lightblue"><em>p</em></td>' +
            '<td align="center" bgcolor="lightblue">T</td>' +
            '<td align="center" bgcolor="lightblue">F</td></tr><tr>' +
            '<td align="right" bgcolor="lightblue"><em>q</em></td><td align="center"></td>' +
            '<td align="center"></td></tr><tr><td align="right" bgcolor="lightblue">T</td>' +
            '<td></td><td align="center">');
}

function writeTruthTableProblem(title, ansArr) { // 2 by 2 truth table problem
    var opt = ['T','F'];
    document.writeln(truthTableHeader(title));
    var solArr = new Array(ansArr.length);
    if ( (typeof(ansArr)).toLowerCase() == 'function') {
        trueArr = [true, false];
        for (var i=0; i < 2; i++ ) {
            for (var j=0; j < 2; j++) {
                var inx = j + 2*i;
                if (ansArr(trueArr[j], trueArr[i])) {
                    solArr[inx] = 'a';
                } else {
                    solArr[inx] = 'b';
                }
            }
        }
    } else {
        for (var i=0; i < ansArr.length; i++) {
            if (ansArr[i]) {
                solArr[i] = 'a';
            } else {
                solArr[i] = 'b';
            }
        }
    }
    writeSelectExercise(false,qCtr++,opt,solArr[0]);
    document.writeln('</td><td align="center">');
    writeSelectExercise(false,qCtr++,opt,solArr[1]);
    document.writeln('</td></tr><tr><td align="right" bgcolor="lightblue">F</td><td></td>' +
                     '<td align="center">');
    writeSelectExercise(false,qCtr++,opt,solArr[2]);
    document.writeln('</td><td align="center">');
    writeSelectExercise(false,qCtr++,opt,solArr[3]);
    document.writeln('</td><tr></table></center></div>');
}


// ===============================================
// ===formatting functions and html generators====
// ===============================================


function commify(num) { // punctuate number strings greater than 1,000 in magnitude
    var str;
    var strA = (removeAllBlanks(num.toString())).toLowerCase();
    if ( (strA.indexOf('e') > -1) || (strA.indexOf('d') > -1) ) {
        str = strA;  // don't mess with exponential notation
    } else {
        str = strA;
        var curLoc = str.length;
        if ( str.indexOf('.') > -1 ) {
            curLoc = str.indexOf('.');
        }
        var negSign = str.indexOf('-');
        for (var loc = curLoc-4; loc > negSign; loc -= 3) {
            str = str.substr(0,loc+1) + ',' + str.substr(loc+1, str.length);
        }
    }
    return(str);
}
            
            
        
function writeBlankLines(k) {  // blank space
    if ( (typeof(k) == 'undefined') || (k == null) || (k < 0) ) {
        k = 1;
    }
    for (var i=0; i < k; i++) {
        document.writeln('<p>&nbsp;</p>');
    }
}

function roundToDig(num,dig) { // rounds a number to dig digits after the decimal place
    var powOfTen = Math.pow(10,dig);
    var fmt = Math.round(num*powOfTen)/powOfTen;
    return(fmt);
}

function doubleToStr(num,dig) {
  // returns a string representation of num, rounded to dig digits after the decimal
    return(removeAllBlanks(roundToDig(num,dig).toString()));
}

function doubleToRange(num,fudge) {
  // returns a string range of num +/- fudge, separated by a colon
    var dig = -Math.floor(Math.log(Math.abs(fudge))/Math.log(10)) + 1;
    var s = roundToDig(num,dig);
    var range = doubleToStr(s - Math.abs(fudge),dig) + ":" +
                          doubleToStr(s+ Math.abs(fudge),dig);
    range = range.replace(/ /g,'');
    return(range);
}

function numToRange(num,fudge) {
  // returns a string range of num +/- fudge, separated by a colon
    if ( (typeof(fudge) == 'undefined') || (fudge == null) ) {
        fudge = fudgeFactor*num;
    }
    if (fudge == 0) {
        fudge = absFudge;
    }
    return(doubleToRange(num,Math.abs(fudge)));
}

function numToOrdinal(num) { // turns integer into string, appends appropriate suffix
    var st = (roundToDig(num,0)).toString();
    var suffArray = ["th","st","nd","rd","th","th","th","th","th","th"];
    var finalDig = parseInt(st.substr(st.length-1,st.length));
    return(st + suffArray[finalDig]);
}

function listToTable(header,list,orientation,centering,print,ft) {
  // formats an array of arrays as an html table
    if (typeof(centering) == 'undefined' || centering == null) {
        centering = "right";
    }
    if (typeof(print) == 'undefined') {
        print = true;
    }
    if (typeof(ft) == 'undefined' || ft == null) {
        ft = "";
        eft = "";
    } else {
        ft = '<font size="' + ft + '">';
        eft = '</font>';
    }
    var rows = list.length;
    var cols;
    if (typeof(list[0]) != 'object') {
        cols=null;
    } else {
        cols = list[0].length;
    }
    var str = "<div align=\"center\"><center><table border=\"2\">";
    if (cols == null || cols == 1) {
        str +="<tr>";
        str +="<td align=\"" + centering + "\"> " + ft + header +eft + "</td>\n";
        if (orientation == "standard") {
            for (var j=0; j < rows; j++) {
                str +="<td align=\"" + centering + "\"> " + ft + list[j] + eft + " </td>\n";
            }
            str +="</tr>";
        } else if (orientation == "transpose") {
            str +="</tr>";
            for (var j=0; j < rows; j++) {
                str +="<tr>";
                str +="<td align=\"" + centering + "\"> " + ft + list[j] + eft + " </td>\n";
                str +="</tr>";
            }
        } else {
            alert("Error 1 in irGrade.listToTable: unsupported orientation " + orientation);
        }
    } else {
        if (orientation == "standard") {
            for (var j = 0; j < rows; j++) {
                str += "<tr>";
                str += "<td align=\"" + centering + "\">" + ft + header[j] + eft + "</td>\n";
                for (var i=0; i < cols; i++) {
                    str += "<td align=\"" + centering + "\">" +
                        ft + list[j][i] + eft + "</td>";
                }
                str +="</tr>";
            }
         } else if (orientation == "transpose") {
            str += "<tr>";
            for (var i=0; i < header.length; i++) {
                str += "<td align=\"" + centering + "\">" + ft + header[i] + eft + "</td>\n";
            }
            str += "</tr>";
            for (var j = 0; j < cols; j++) {
                str += "<tr>";
                for (var i=0; i < rows; i++) {
                    str += "<td align=\"" + centering + "\">" + ft + list[i][j] + eft + "</td>\n";
                }
                str +="</tr>";
            }
         } else {
            alert("Error 2 in irGrade.listToTable: unsupported orientation " + orientation);
         }
    }
    str += "</table></center></div>";
    if (print) {
        document.writeln(str);
        return(true);
    } else {
        return(str);
    }
 }

function arrayToRow(v,alignment) {
 // makes a row of a table from the elements of the array v, with specified alignment
    document.writeln("<tr>");
    for (var i=0; i < v.length; i++) {
        document.write('<td align=\"right\">');
        document.write(v[i].toString());
        document.writeln("</td>");
    }
    document.writeln("</tr>");
    return(true);
}

// ============================================================================
// ========================= STATISTICAL SUBROUTINES ==========================

function mean(list) { // computes the mean of the list
    return(vSum(list)/list.length);
}

function vMult(a, list) { // multiply a vector times a scalar
    var list2 = new Array(list.length);
    for (var i=0; i < list.length; i++) {
        list2[i] = a*list[i];
    }
    return(list2);
}

function vPointwiseMult(list1, list2) { // componentwise multiplication of two vectors
    var list3 = Math.NaN;
    if (list1.length != list2.length) {
        alert('Error #1 in irGrade.vPointwiseMult: vector lengths do not match!');
    } else {
        list3 = new Array(list1.length);
        for (var i=0; i < list1.length; i++) {
            list3[i] = list1[i]*list2[i];
        }
    }
    return(list3);
}
        
function vFloor(list) { // takes floor of all components
    var list2 = new Array(list.length);
    for (var i = 0; i < list.length; i++) {
        list2[i] = Math.floor(list[i]);
    }
    return(list2);
}

function vCeil(list) { // takes ceil of all components
    var list2 = new Array(list.length);
    for (var i = 0; i < list.length; i++) {
        list2[i] = Math.ceil(list[i]);
    }
    return(list2);
}

function vRoundToInts(list) { // round all components to the nearest int
    var list2 = new Array(list.length);
    var tmp;
    for (var i = 0; i < list.length; i++) {
        list2[i] = Math.floor(list[i]);
        if (list[i] - list2[i] >= 0.5) {
            list2[i]++;
        }
    }
    return(list2);
}

function vSum(list) { // computes the sum of the elements of list
    var tot = 0.0;
    for (var i = 0; i < list.length; i++) {
        tot += list[i];
    }
    return(tot);
}

function vCum(list) { // vector of cumulative sum
    var list2 = list;
    for (var i = 1; i < list.length; i++ ) {
        list2[i] += list2[i-1];
    }
    return(list2);
}

function vZero(n) { // returns a vector of zeros of length n
    var list = new Array(n);
    for (var i=0; i < n; i++) {
        list[i] = 0.0;
    }
    return(list);
}

function vOne(n) { // returns a vector of ones of length n
    var list = new Array(n);
    for (var i=0; i < n; i++) {
        list[i] = 1.0;
    }
    return(list);
}

function twoNorm(list) { // two norm of a vector
    var tn = 0.0;
    for (var i=0; i < list.length; i++) {
        tn += list[i]*list[i];
    }
    return(Math.sqrt(tn));
}

function convolve(a,b) { // convolve two lists
    var c = new Array(a.length + b.length - 1);
    var left; var right;
    for (var i=0; i < a.length + b.length - 1; i++) {
        c[i] = 0;
        right = Math.min(i+1, a.length);
        left = Math.max(0, i - b.length + 1);
        for (var j=left; j < right; j++) {
            c[i] += a[j]*b[b.length - i - 1 + j];
        }
    }
    return(c);
}

function nFoldConvolve(a,n) {
    var b = a;
    for (var i=0; i < n; i++ ) {
        b = convolve(b,a);
    }
    return(b);
}

function numberLessThan(a,b) { // numerical ordering for javascript sort function
    var diff = parseFloat(a)-parseFloat(b);
    if (diff < 0) {
        return(-1);
    } else if (diff == 0) {
        return(0);
    } else {
        return(1);
    }
}

function numberGreaterThan(a,b) { // numerical ordering for javascript sort function
    var diff = parseFloat(a)-parseFloat(b);
    if (diff < 0) {
        return(1);
    } else if (diff == 0) {
        return(0);
    } else {
        return(-1);
    }
}

function sd(list) { // computes the SD of the list
    ave = mean(list);
    ssq = 0;
    for (var i = 0; i < list.length; i++) {
        ssq += (list[i] - ave)*(list[i] - ave);
    }
    ssq = Math.sqrt(ssq/list.length);
    return(ssq);
}

function sampleSd(list) { // computes the sample SD of the list
    ave = mean(list);
    ssq = 0;
    for (var i = 0; i < list.length; i++) {
        ssq += (list[i] - ave)*(list[i] - ave);
    }
    ssq = Math.sqrt(ssq/(list.length - 1.0));
    return(ssq);
}

function corr(list1, list2) {
// computes the correlation coefficient of list1 and list2
    if (list1.length != list2.length) {
        alert("Error #1 in irGrade.corr(): lists have different lengths!");
        return(Math.NaN);
    } else {
        var ave1 = mean(list1);
        var ave2 = mean(list2);
        var sd1 = sd(list1);
        var sd2 = sd(list2);
        var cc = 0.0;
        for (var i=0; i < list1.length; i++) {
            cc += (list1[i] - ave1)*(list2[i] - ave2);
        }
        cc /= sd1*sd2*list1.length;
        return(cc);
    }
}

function percentile(list,p) { // finds the pth percentile of list
    var n = list.length;
    var sList = new Array(n);
    for (var i=0; i < n; i++) sList[i] = list[i].valueOf();
    sList.sort(numberLessThan);
    var ppt = Math.max(Math.ceil(p*n/100),1);
    return(sList[ppt-1]);
}

function listOfRandSigns(n) { // random +-1 vector
    var list = new Array(n);
    for (var i=0; i < n; i++) {
        var rn = rand.next();
        if (rn < 0.5) {
            list[i] = -1;
        } else {
            list[i] = 1;
        }
    }
    return(list);
}

function listOfRandUniforms(n, lo, hi) { // n random variables uniform on (lo, hi)
    if ( (typeof(lo) == 'undefined') || (lo == null) ) {
        lo = 0.0;
    }
    if ( (typeof(hi) == 'undefined') || (hi == null) ) {
            hi = 1.0;
    }
    var list = new Array(n);
    for (var i=0; i < n; i++) {
        list[i] = lo + (hi-lo)*rand.next();
    }
    return(list);
}

function listOfRandInts(n, lo, hi) { // n random integers between lo and hi
    var list = new Array(n);
    for (var i=0; i < n; i++) {
        list[i] = Math.floor((hi+1 - lo)*rand.next()) + lo;
    }
    return(list);
}

function listOfDistinctRandInts(n, lo, hi) { // n dintinct random integers between lo and hi
    var list = new Array(n);
    var trial;
    var i=0;
    var unique;
    while (i < n) {
        trial = Math.floor((hi+1 - lo)*rand.next()) + lo;
        unique = true;
        for (var j = 0; j < i; j++) {
            if (trial == list[j]) unique = false;
        }
        if (unique) {
            list[i] = trial;
            i++;
        }
    }
    return(list);
}

function randomSample(list, ssize, replace) {
  // sample from list, size ssize w/ or w/o replacement.
  // default is without replacement
    var sample = new Array();
    var indices = new Array();
    if (replace != null && replace ) {
        indices = listOfRandInts(ssize,0,list.length-1);
    } else {
        indices = listOfDistinctRandInts(ssize,0,list.length - 1);
    }
    for (var i=0; i < ssize; i++) {
        sample[i] = list[indices[i]];
    }
    return(sample) ;
}

function multinomialSample(pVec, n) { // multinomial sample of size n with probabilities pVec
    pVec = vMult(1.0/vSum(pVec), pVec); // renormalize in case
    var pCum = vCum(pVec);
    var counts = vZero(pVec.length);
    var rv;
    var inx;
    for (var i=0; i < n; i++) {
        rv = rand.next();
        inx = 0;
        while ( (rv > pCum[inx]) && (inx < n) ) {
            inx++;
        }
        counts[inx]++;
    }
    return(counts);
}

function normPoints(n, mu, s, dig) {   // n normals with expected value mu, sd s, rounded to dig
    var round = true;
    if ( (typeof(dig) == 'undefined') || (dig == null) ) {
        var round = false;
    }
    var xVal = new Array(n);
    if (round) {
        for (var i=0; i < n; i++) {
            xVal[i] = roundToDig(mu + s*rNorm(),dig);
        }
    } else {
        for (var i=0; i < n; i++) {
            xVal[i] = mu + s*rNorm(),dig;
        }
    }
    return(xVal);
}

function cNormPoints(n, r) {
 // generate pseudorandom normal bivariate w/ specified realized correlation coefficient
    var xVal = new Array(n);
    var yVal = new Array(n);
    for (var i=0; i<n ; i++ ) {
        xVal[i]= rNorm();
        yVal[i] = rNorm();
    }
    var rAtt = corr(xVal, yVal);
    var s = sgn(rAtt)*sgn(r);
    var xBarAtt = mean(xVal);
    var yBarAtt = mean(yVal);
    var xSdAtt = sd(xVal);
    var ySdAtt = sd(yVal);
    var pred = new Array(n);
    var resid = new Array(n);
    for (var i=0; i < n; i++) {
        xVal[i] = (xVal[i] - xBarAtt)/xSdAtt;
        pred[i] = s*rAtt*xVal[i]*ySdAtt+ yBarAtt;
        resid[i] = s*yVal[i] - pred[i];
    }
    var resNrm = rms(resid);
    for (var i = 0; i < n; i++) {
        yVal[i] = Math.sqrt(1.0-r*r)*resid[i]/resNrm + r*xVal[i];
    }
    var ymnmx = vMinMax(yVal);
    var xmnmx = vMinMax(xVal);
    var xscl = 9.5/(xmnmx[1] - xmnmx[0]);
    var yscl = 9.5/(ymnmx[1] - ymnmx[0]);
    for (var i=0; i < n; i++) {
        xVal[i] = (xVal[i] - xmnmx[0]) * xscl  + 1.0;
        yVal[i] = (yVal[i] - ymnmx[0]) * yscl + 1.0;
    }
    var lists = new Array(xVal,yVal);
    return(lists);
}// ends cNormPoints

function listOfRandReals(n,lo,hi) { // n-vector of uniforms on [lo, hi]
    var list = new Array(n);
    for (var i=0; i < n; i++) {
        list[i] = (hi - lo)*rand.next() + lo;
    }
    return(list);
}

function rNorm() {  // standard normal pseudorandom variable
    var y = normInv(rand.next());
    return(y);
} // ends rNorm()

function normCdf(y) { // normal distribution cumulative distribution function
   return(0.5*erfc(-y*0.7071067811865475));
}

function erfc(x) { // error function
     var xbreak = 0.46875;     // for normal cdf
// coefficients for |x| <= 0.46875
    var a = [3.16112374387056560e00, 1.13864154151050156e02,
             3.77485237685302021e02, 3.20937758913846947e03,
             1.85777706184603153e-1];
    var b = [2.36012909523441209e01, 2.44024637934444173e02,
            1.28261652607737228e03, 2.84423683343917062e03];
// coefficients for 0.46875 <= |x| <= 4.0
    var c = [5.64188496988670089e-1, 8.88314979438837594e00,
             6.61191906371416295e01, 2.98635138197400131e02,
             8.81952221241769090e02, 1.71204761263407058e03,
             2.05107837782607147e03, 1.23033935479799725e03,
             2.15311535474403846e-8];
    var d = [1.57449261107098347e01, 1.17693950891312499e02,
             5.37181101862009858e02, 1.62138957456669019e03,
             3.29079923573345963e03, 4.36261909014324716e03,
             3.43936767414372164e03, 1.23033935480374942e03];
// coefficients for |x| > 4.0
    var p = [3.05326634961232344e-1, 3.60344899949804439e-1,
             1.25781726111229246e-1, 1.60837851487422766e-2,
             6.58749161529837803e-4, 1.63153871373020978e-2];
    var q = [2.56852019228982242e00, 1.87295284992346047e00,
             5.27905102951428412e-1, 6.05183413124413191e-2,
             2.33520497626869185e-3];
    var y, z, xnum, xden, result, del;

/*
Translation of a FORTRAN program by W. J. Cody,
Argonne National Laboratory, NETLIB/SPECFUN, March 19, 1990.
The main computation evaluates near-minimax approximations
from "Rational Chebyshev approximations for the error function"
by W. J. Cody, Math. Comp., 1969, PP. 631-638.
*/

//  evaluate  erf  for  |x| <= 0.46875

    if(Math.abs(x) <= xbreak) {
        y = Math.abs(x);
        z = y * y;
        xnum = a[4]*z;
        xden = z;
        for (var i = 0; i< 3; i++) {
            xnum = (xnum + a[i]) * z;
            xden = (xden + b[i]) * z;
        }
        result = 1.0 - x* (xnum + a[3])/ (xden + b[3]);
    } else if (Math.abs(x) <= 4.0) {
        y = Math.abs(x);
        xnum = c[8]*y;
        xden = y;
        for (var i = 0; i < 7; i++) {
            xnum = (xnum + c[i])* y;
            xden = (xden + d[i])* y;
        }
        result = (xnum + c[7])/(xden + d[7]);
        if (y > 0.0) {
            z = Math.floor(y*16)/16.0;
        } else {
            z = Math.ceil(y*16)/16.0;
        }
        del = (y-z)*(y+z);
        result = Math.exp(-z*z) * Math.exp(-del)* result;
    } else {
        y = Math.abs(x);
        z = 1.0 / (y*y);
        xnum = p[5]*z;
        xden = z;
        for (var i = 0; i < 4; i++) {
            xnum = (xnum + p[i])* z;
            xden = (xden + q[i])* z;
        }
        result = z * (xnum + p[4]) / (xden + q[4]);
        result = (1.0/Math.sqrt(Math.PI) -  result)/y;
        if (y > 0.0) {
            z = Math.floor(y*16)/16.0;
        } else {
            z = Math.ceil(y*16)/16.0;
        }
        del = (y-z)*(y+z);
        result = Math.exp(-z*z) * Math.exp(-del) * result;
    }
    if (x < -xbreak) {
        result = 2.0 - result;
    }
    return(result);
}

function normInv(p) {
    if ( p == 0.0 ) {
        return(Math.NEGATIVE_INFINITY);
    } else if ( p >= 1.0 ) {
        return(Math.POSITIVE_INFINITY);
    } else {
        return(Math.sqrt(2.0) * erfInv(2*p - 1));
    }
}

function erfInv(y) {
    var a = [ 0.886226899, -1.645349621, 0.914624893, -0.140543331];
    var b = [-2.118377725, 1.442710462, -0.329097515, 0.012229801];
    var c = [-1.970840454, -1.624906493, 3.429567803, 1.641345311];
    var d = [ 3.543889200, 1.637067800];
    var y0 = 0.7;
    var x = 0;
    var z = 0;
    if (Math.abs(y) <= y0) {
        z = y*y;
        x = y * (((a[3]*z+a[2])*z+a[1])*z+a[0])/
         ((((b[3]*z+b[2])*z+b[1])*z+b[0])*z+1.0);
    } else if (y > y0 && y < 1.0) {
        z = Math.sqrt(-Math.log((1-y)/2));
        x = (((c[3]*z+c[2])*z+c[1])*z+c[0]) / ((d[1]*z+d[0])*z+1);
    } else if (y < -y0 && y > -1) {
        z = Math.sqrt(-Math.log((1+y)/2));
        x = -(((c[3]*z+c[2])*z+c[1])*z+c[0])/ ((d[1]*z+d[0])*z+1);
    }
    x = x - (1.0 - erfc(x) - y) / (2/Math.sqrt(Math.PI) * Math.exp(-x*x));
    x = x - (1.0 - erfc(x) - y) / (2/Math.sqrt(Math.PI) * Math.exp(-x*x));

    return(x);
} // ends erfInv

function betaCdf( x,  a,  b) {
   if (a <= 0 || b <= 0) {
      return(Math.NaN);
   } else if (x >= 1) {
      return(1.0);
   } else if ( x > 0.0) {
      return(Math.min(incBeta(x ,a ,b),1.0));
   } else {
      return(0.0);
   }
}

function betaPdf( x,  a,  b) {
    if (a <= 0 || b <= 0 || x < 0 || x > 1) {
        return(Math.NaN);
    } else if ((x == 0 && a < 1) || (x == 2 && b < 1)) {
        return(Math.POSITIVE_INFINITY);
    } else if (!(a <= 0 || b <= 0 || x <= 0 || x >= 1)) {
        return(Math.exp((a - 1)*Math.log(x) + (b-1)*Math.log(1 - x) - lnBeta(a,b)));
    } else {
        return(0.0);
    }
}

function lnBeta( x, y) {
    return(lnGamma(x) + lnGamma(y) - lnGamma(x+y));
}

function betaInv( p,  a,  b) {
    if (p < 0 || p > 1 || a <= 0 || b <= 0) {
        return(Math.NaN);
    } else if ( p == 0 ) {
        return(Math.NEGATIVE_INFINITY);
    } else if ( p == 1) {
        return(Math.POSITIVE_INFINITY);
    } else {
        var maxIt = 100;
        var it = 0;
        var tol = Math.sqrt(eps);
        var work = 1.0;
        var next;
        var x;
        if (a == 0.0 ) {
            x = Math.sqrt(eps);
        } else if ( b == 0.0) {
            x = 1 - Math.sqrt(eps);
        } else {
            x = a/(a+b);
        }
        while (Math.abs(work) > tol*Math.abs(x) && Math.abs(work) > tol && it < maxIt) {
           it++;
           work = (betaCdf(x,a,b) - p)/betaPdf(x,a,b);
           next =  x - work;
           while (next < 0 || next > 1) {
               work = work/2;
               next = x - work;
           }
           x = next;
         }
         return(x);
     }
}

function lnGamma(x) {
/*  natural ln(gamma(x)) without computing gamma(x)
    P.B. Stark, stark@stat.berkeley.edu

      JavaScript subroutine is based on a MATLAB program by C. Moler,
      in turn based on a FORTRAN program by W. J. Cody,
      Argonne National Laboratory, NETLIB/SPECFUN, June 16, 1988.

      References:

      1) W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for
         the Natural Logarithm of the Gamma Function,' Math. Comp. 21,
         1967, pp. 198-203.

      2) K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May,
         1969.

      3) Hart, Et. Al., Computer Approximations, Wiley and sons, New
         York, 1968.
*/

     var d1 = -5.772156649015328605195174e-1;
     var p1 = [4.945235359296727046734888e0, 2.018112620856775083915565e2,
           2.290838373831346393026739e3, 1.131967205903380828685045e4,
           2.855724635671635335736389e4, 3.848496228443793359990269e4,
           2.637748787624195437963534e4, 7.225813979700288197698961e3];
     var q1 = [6.748212550303777196073036e1, 1.113332393857199323513008e3,
           7.738757056935398733233834e3, 2.763987074403340708898585e4,
           5.499310206226157329794414e4, 6.161122180066002127833352e4,
           3.635127591501940507276287e4, 8.785536302431013170870835e3];
     var d2 = 4.227843350984671393993777e-1;
     var p2 = [4.974607845568932035012064e0, 5.424138599891070494101986e2,
           1.550693864978364947665077e4, 1.847932904445632425417223e5,
           1.088204769468828767498470e6, 3.338152967987029735917223e6,
           5.106661678927352456275255e6, 3.074109054850539556250927e6];
     var q2 = [1.830328399370592604055942e2, 7.765049321445005871323047e3,
           1.331903827966074194402448e5, 1.136705821321969608938755e6,
           5.267964117437946917577538e6, 1.346701454311101692290052e7,
           1.782736530353274213975932e7, 9.533095591844353613395747e6];
     var d4 = 1.791759469228055000094023e0;
     var p4 = [1.474502166059939948905062e4, 2.426813369486704502836312e6,
           1.214755574045093227939592e8, 2.663432449630976949898078e9,
           2.940378956634553899906876e10, 1.702665737765398868392998e11,
           4.926125793377430887588120e11, 5.606251856223951465078242e11];
     var q4 = [2.690530175870899333379843e3, 6.393885654300092398984238e5,
           4.135599930241388052042842e7, 1.120872109616147941376570e9,
           1.488613728678813811542398e10, 1.016803586272438228077304e11,
           3.417476345507377132798597e11, 4.463158187419713286462081e11];
     var c = [-1.910444077728e-03, 8.4171387781295e-04,
          -5.952379913043012e-04, 7.93650793500350248e-04,
          -2.777777777777681622553e-03, 8.333333333333333331554247e-02,
           5.7083835261e-03];

     var lng = Math.NaN;
     var mach = 1.e-12;
     var den = 1.0;
     var num = 0;
     var xm1, xm2, xm4;

   if (x < 0) {
       return(lng);
   } else if (x <= mach) {
       return(-Math.log(x));
   } else if (x <= 0.5) {
      for (var i = 0; i < 8; i++) {
            num = num * x + p1[i];
            den = den * x + q1[i];
      }
      lng = -Math.log(x) + (x * (d1 + x * (num/den)));
   } else if (x <= 0.6796875) {
      xm1 = x - 1.0;
      for (var i = 0; i < 8; i++) {
         num = num * xm1 + p2[i];
         den = den * xm1 + q2[i];
      }
      lng = -Math.log(x) + xm1 * (d2 + xm1*(num/den));
   } else if (x <= 1.5) {
      xm1 = x - 1.0;
      for (var i = 0; i < 8; i++) {
         num = num*xm1 + p1[i];
         den = den*xm1 + q1[i];
      }
      lng = xm1 * (d1 + xm1*(num/den));
   } else if (x <= 4.0) {
      xm2 = x - 2.0;
      for (var i = 0; i<8; i++) {
         num = num*xm2 + p2[i];
         den = den*xm2 + q2[i];
      }
      lng = xm2 * (d2 + xm2 * (num/den));
   } else if (x <= 12) {
      xm4 = x - 4.0;
      den = -1.0;
      for (var i = 0; i < 8; i++)  {
         num = num * xm4 + p4[i];
         den = den * xm4 + q4[i];
      }
      lng = d4 + xm4 * (num/den);
   } else {
      var r = c[6];
      var xsq = x * x;
      for (var i = 0; i < 6; i++) {
         r = r / xsq + c[i];
      }
      r = r / x;
      var lnx = Math.log(x);
      var spi = 0.9189385332046727417803297;
      lng = r + spi - 0.5*lnx + x*(lnx-1);
    }
    return(lng);
} // ends lnGamma


function normPdf( mu,  sigma, x) {
     return(Math.exp(-(x-mu)*(x-mu)/(2*sigma*sigma))/
            (Math.sqrt(2*Math.PI)*sigma));
} // ends normPdf


function tCdf(df, x) { // cdf of Student's t distribution with df degrees of freedom
    var ans;
    if (df < 1) {
        ans = Math.NaN;
    } else if (x == 0.0) {
        ans = 0.5;
    } else if (df == 1) {
        ans = .5 + Math.atan(x)/Math.PI;
    } else if (x > 0) {
        ans = 1 - (incBeta(df/(df+x*x), df/2.0, 0.5))/2;
    } else if (x < 0) {
        ans = incBeta(df/(df+x*x), df/2.0, 0.5)/2;
    }
    return(ans);
}

function tInv(p, df ) { // inverse Student-t distribution with
                                              // df degrees of freedom
    var z;
    if (df < 0 || p < 0) {
        return(Math.NaN);
    } else if (p == 0) {
        return(Math.NEGATIVE_INFINITY);
    } else if (p == 1) {
        return(Math.POSITIVE_INFINITY);
    } else if (df == 1) {
        return(Math.tan(Math.PI*(p-0.5)));
    } else if ( p >= 0.5) {
        z = betaInv(2.0*(1-p),df/2.0,0.5);
        return(Math.sqrt(df/z - df));
    } else {
        z = betaInv(2.0*p,df/2.0,0.5);
        return(-Math.sqrt(df/z - df));
    }
}

function incBeta(x, a, b) { // incomplete beta function
       // I_x(z,w) = 1/beta(z,w) * integral from 0 to x of t^(z-1) * (1-t)^(w-1) dt
       // Ref: Abramowitz & Stegun, Handbook of Mathemtical Functions, sec. 26.5.
    var res;
    if (x < 0 || x > 1) {
        res = Math.NaN;
    } else {
        res = 0;
        var bt = Math.exp(lnGamma(a+b) - lnGamma(a) - lnGamma(b) +
                    a*Math.log(x) + b*Math.log(1-x));
        if (x < (a+1)/(a+b+2)) {
            res = bt * betaGuts(x, a, b) / a;
        } else {
            res = 1 - bt*betaGuts(1-x, b, a) / b;
        }
    }
    return(res);
}

function betaGuts( x, a, b) { // guts of the incomplete beta function
    var ap1 = a + 1;
    var am1 = a - 1;
    var apb = a + b;
    var am = 1;
    var bm = am;
    var y = am;
    var bz = 1 - apb*x/ap1;
    var d = 0;
    var app = d;
    var ap = d;
    var bpp = d;
    var bp = d;
    var yold = d;
    var m = 1;
    var t;
    while (y-yold > 4*eps*Math.abs(y)) {
       t = 2 * m;
       d = m * (b - m) * x / ((am1 + t) * (a + t));
       ap = y + d * am;
       bp = bz + d * bm;
       d = -(a + m) * (apb + m) * x / ((a + t) * (ap1 + t));
       app = ap + d * y;
       bpp = bp + d * bz;
       yold = y;
       am = ap / bpp;
       bm = bp / bpp;
       y = app / bpp;
       if (m == 1) bz = 1;
       m++;
    }
    return(y);
}

function chi2Cdf( x, df ) {
    return(gammaCdf(x,df/2,2));
}

function chi2Inv( p, df ) { // kluge for chi-square quantile function.
    var guess = Math.NaN;
    if (p == 0.0) {
        guess = 0.0;
    } else if ( p == 1.0 ) {
        guess = Math.POSITIVE_INFINITY;
    } else if ( p < 0.0 ) {
        guess = Math.NaN;
    } else {
        var tolAbs = 1.0e-8;
        var tolRel = 1.0e-3;
        guess = Math.max(0.0, df + Math.sqrt(2*df)*normInv(p)); // guess from normal approx    
        var currP = chi2Cdf( guess, df);
        var loP = currP;
        var hiP = currP;
        var guessLo = guess;
        var guessHi = guess;
        while (loP > p) { // step down
            guessLo = 0.8*guessLo;
            loP = chi2Cdf( guessLo, df);
        }
        while (hiP < p) { // step up
            guessHi = 1.2*guessHi;
            hiP = chi2Cdf( guessHi, df);
        }
        guess = (guessLo + guessHi)/2.0;
        currP = chi2Cdf( guess, df);
        while ( (Math.abs(currP - p) > tolAbs) || (Math.abs(currP - p)/p > tolRel) ) { // bisect
            if ( currP < p ) {
                guessLo = guess;
            } else {
                guessHi = guess;
            }  
            guess = (guessLo + guessHi)/2.0;
            currP = chi2Cdf(guess, df);
        }
    }
    return(guess);
}
        

function gammaCdf( x,  a,  b) { // gamma distribution CDF.
    var p = Math.NaN;
    if (a <= 0 || b <= 0) {
    } else if (x <= 0) {
        p = 0.0;
    } else {
        p = Math.min(incGamma(x/b, a), 1.0);
    }
    return(p);
}

function incGamma( x,  a) {
    var inc = 0;
    var gam = lnGamma(a+rmin);
    if (x == 0) {
        inc = 0;
    } else if (a == 0) {
        inc = 1;
    } else if (x < a+1) {
        var ap = a;
        var sum = 1.0/ap;
        var del = sum;
        while (Math.abs(del) >= 10*eps*Math.abs(sum)) {
            del *= x/(++ap);
            sum += del;
        }
        inc = sum * Math.exp(-x + a*Math.log(x) - gam);
    } else if (x >= a+1) {
       var a0 = 1;
       var a1 = x;
       var b0 = 0;
       var b1 = 1;
       var fac = 1;
       var n = 1;
       var g = 1;
       var gold = 0;
       var ana;
       var anf;
       while (Math.abs(g-gold) >= 10*eps*Math.abs(g)) {
            gold = g;
            ana = n - a;
            a0 = (a1 + a0 *ana) * fac;
            b0 = (b1 + b0 *ana) * fac;
            anf = n*fac;
            a1 = x * a0 + anf * a1;
            b1 = x * b0 + anf * b1;
            fac = 1.0 / a1;
            g = b1 * fac;
            n++;
       }
       inc = 1 - Math.exp(-x + a*Math.log(x) - gam) * g;
    }
    return(inc);
}

function poissonPmf( lambda, k) {
    var p = 0.0;
    if (k >= 0) {
        p = Math.exp(-lambda)*Math.pow(lambda,k)/factorial(k);
    }
    return(p);
}

function poissonCdf( lambda, k) {
    var p = 0;
    var b = 0;
    var m = 0;
    while (m <= k) {
        b += Math.pow(lambda, m++)/factorial(k);
    }
    p += Math.exp(-lambda)*b;
    return(p);
}

function poissonTail(lambda, k) {
    return(1.0-poissonCdf(lambda, k-1));
}


function factorial(n) { // computes n!
    var fac=1;
    for (var i=n; i > 1; i--) {fac *= i;}
    return(fac);
}

function binomialCoef(n,k) { // computes n choose k
    if (n < k || n < 0) {
        return(0.0);
    } else if ( k == 0 || n == 0 || n == k) {
        return(1.0);
    } else {
        var minnk = Math.min(k, n-k);
        var coef = 1;
        for (var j = 0; j < minnk; j++) {
            coef *= (n-j)/(minnk-j);
        }
        return(coef);
    }
}

function binomialPmf(n, p, k) {  // binomial pmf at k.
    var pmf = binomialCoef(n,k)*Math.pow(p,k)*Math.pow((1-p),(n-k));
    return(pmf);
}

function binomialCdf(n, p, k) {  // binomial CDF:  Pr(X <= k), X~B(n,p)
    if (k < 0) {
        return(0.0);
    } else if (k >= n) {
        return(1.0);
    } else {
        var cdf = 0.0;
        for (var i = 0; i <= k; i++) {
            cdf += binomialPmf(n, p, i);
        }
        return(cdf);
    }
}

function binomialTail(n,p,k) { // binomial tail probability Pr(X >= k), X~B(n,p)
    if (k < 0) {
        return(1.0);
    } else if (k >= n) {
        return(0.0);
    } else {
        var tailP = 0.0;
        for (var i = k; i <= n; i++) {
            tailP += binomialPmf(n, p, i);
        }
        return(tailP);
    }
}

function binomialInv(n, p, pt) { // binomial percentile function
    var t = 0;
    if (pt < 0 || pt > 1) {
        t = NaN;
    } else if (pt == 0.0) {
        t = 0;
    } else if (pt == 1.0) {
        t = n;
    } else {
        var t = 0;
        var pc = 0.0;
        while ( pc < pt ) {
            pc += binomialPmf(n, p, t++);
        }
        t -= 1;
    }
    return(t);
}

function multinomialCoef(list, n) { // multinomial coefficient.
// WARNING:  not very stable algorithm; avoid for large n.
    var val = 0; 
    var lmn = vMinMax(list);
    if (typeof(n) == 'undefined' || n == null) {
        n = vSum(list);
    }
    if (lmn[0] < 0.0) {
        alert('Error #1 in irGrade.multinomialCoef: a number of outcomes is negative!');
    } else if (n == vSum(list)) {
        val = factorial(n);
        for (var i=0; i < list.length; i++) {
            val /= factorial(list[i]);
        }
    }
    return(val);        
}

function multinomialPmf(olist, plist, n) { // multinomial pmf; not stable algorithm
    var val = 0.0;
    var pmn = vMinMax(plist);
    var omn = vMinMax(olist);
    if (typeof(n) == 'undefined' || n == null) {
        n = vSum(olist);
    }
    if (olist.length != plist.length) {
        alert('Error #1 in irGrade.multinomialPmf: length of outcome and probability vectors ' +
               'do not match!');
    } else if (pmn[0] < 0.0) {
        alert('Error #2 in irGrade.multinomialPmf: a probability is negative!');
    } else if (omn[0] < 0.0) {
        alert('Error #3 in irGrade.multinomialPmf: a number of outcomes is negative!');
    } else if (n == vSum(olist)) {
        var pl = vMult(1.0/vSum(plist), plist);  // just in case
        val = factorial(n);
        for (var i=0; i< olist.length; i++) {
            val *= Math.pow(pl[i], olist[i])/factorial(olist[i]);
        }
    }
    return(val);
}
            

function geoPmf( p,  k) {
  // chance it takes k trials to the first success in iid Bernoulli(p) trials
  // EX = 1/p; SD(X) = sqrt(1-p)/p
    if (k < 1 || p == 0.0) {
        return(0.0);
    } else {
        return(Math.pow((1-p),k-1)*p);
    }
}

function geoCdf( p, k) {
  // chance it takes k or fewer trials to the first success in iid Bernoulli(p) trials
    if (k < 1 || p == 0.0) {
        return(0.0);
    } else {
        return(1-Math.pow( 1-p, k));
    }
}

function geoTail( p,  k) {
  // chance of k or more trials to the first success in iid Bernoulli(p) trials
    return(1 - geoCdf(p, k-1));
}

function geoInv(p, pt) { // geometric percentile function
    var t = 0;
    if (pt < 0 || pt > 1) {
        t = Math.NaN;
    } else if (pt == 0.0) {
        t = 0;
    } else if (pt == 1.0) {
        t = Math.POSITIVE_INFINITY;
    } else {
        var t = 0;
        var pc = 0.0;
        while ( pc < pt ) {
            pc += geoPmf(p, t++);
        }
    }
    return(t);
}

function hyperGeoPmf( N,  M,  n,  m) {
  // chance of drawing m of M objects in a sample of size n from
  // N objects in all.  p = (M C m)*(N-M C n-m)/(N C n)
  // EX = n*M/N; SD(X)= sqrt((N-n)/(N-1))*sqrt(np(1-p));
    var p;
    if ( n < m || N < M || M < m  || m < 0 || N < 0) {
        return(0.0);
    } else {
        p = binomialCoef(M,m)*binomialCoef(N-M,n-m)/binomialCoef(N,n);
        return(p);
    }
}

function hyperGeoCdf( N,  M,  n,  m) {
  // chance of drawing m or fewer of M objects in a sample of size n from
  // N objects in all
    var p=0.0;
    var mMax = Math.min(m,M);
    mMax = Math.min(mMax,n);
    for (var i = 0; i <= mMax; i++) {
        p += hyperGeoPmf(N, M, n, i);
    }
    return(p);
}

function hyperGeoTail( N,  M,  n,  m) {
  // chance of drawing m or more of M objects in a sample of size n from
  // N objects in all
    var p=0.0;
    for (var i = m; i <= M; i++) {
        p += hyperGeoPmf(N, M, n, i);
    }
    return(p);
}

function negBinomialPmf( p,  s,  t) {
  // chance that the sth success in iid Bernoulli trials is on the tth trial
  // EX = s/p; SD(X) = sqrt(s(1-p))/p
    if (s > t || s < 0) {
        return(0.0);
    }
    var prob = p*binomialPmf(t-1,p,s-1);
    return(prob);
}

function negBinomialCdf( p,  s,  t) {
  // chance the sth success in iid Bernoulli trials is on or before the tth trial
    var prob = 0.0;
    for (var i = s; i <= t; i++) {
        prob += negBinomialPmf(p, s, i);
    }
    return(prob);
}

function pDieRolls(rolls,spots) { // chance that the sum of 'rolls' rolls of a die = 'spots'
    if (rolls > 4) {
        alert("Error #1 in irGrade.pDiceRolls: too many rolls " + rolls + ". ");
        return(Math.NaN);
    } else {  // BRUTE FORCE!
        var found = 0;
        if (spots < rolls || spots > 6*rolls) {return(0.0);}
        var possible = Math.pow(6,rolls);
        if (rolls == 1) {
            return(1/possible);
        } else if (rolls == 2) {
            for (var i=1; i <=6; i++ ) {
                for (var j=1; j <= 6; j++ ) {
                    if (i+j == spots ) {found++;}
                }
            }
        } else if (rolls == 3 ) {
            for (var i=1; i <=6; i++ ) {
                for (var j=1; j<=6; j++ ) {
                    for (var k=1; k<=6; k++ ) {
                        if (i+j+k == spots ) {found++;}
                    }
                }
            }
        } else if (rolls == 4 ) {
            for (var i=1; i <=6; i++ ) {
                for (var j=1; j<=6; j++ ) {
                    for (var k=1; k<=6; k++ ) {
                        for (var m=1; m <=6; m++ ) {
                            if (i+j+k+m == spots ) {found++;}
                        }
                    }
                }
            }
        }
        return(found/possible);
    }
    return(false);
}

function permutations(n,k) { // number of permutations of k of n things
    if (n < k || n < 0) {
        return(0);
    } else if ( k==0 || n == 0) {
        return(1);
    } else {
        var coef=1;
        for (var j=0; j < k; j++) coef *= (n-j);
    }
    return(coef);
}


function sgn(x) {  // signum function
    if (x >= 0) {
        return(1);
    } else if (x < 0) {
        return (-1);
    }
}

function linspace(lo,hi,n) { // n linearly spaced points between lo and hi
    var spaced = new Array(n);
    var dx =(hi-lo)/(n-1);
    for (var i=0; i < n; i++) {
        spaced[i] = lo + i*dx;
    }
    return(spaced);
}

function rms(list) { // rms
    var r = 0;
    for (var i=0; i < list.length; i++) r += list[i]*list[i];
    r /= list.length;
    return(Math.sqrt(r));
}

function vMinMax(list){ // returns min and max of list
    var mn = list[0];
    var mx = list[0];
    for (var i=1; i < list.length; i++) {
        if (mn > list[i]) mn = list[i];
        if (mx < list[i]) mx = list[i];
    }
    var vmnmx =  new Array(mn,mx);
    return(vmnmx);
}

function vMinMaxIndices(list){ // returns min, max, index of min, index of max
    var mn = list[0];
    var indMn = 0;
    var mx = list[0];
    var indMx = 0;
    for (var i=1; i < list.length; i++) {
        if (mn > list[i]) {
            mn = list[i];
            indMn = i;
        }
        if (mx < list[i]) {
            mx = list[i];
            indMx = i;
        }
    }
    var vmnmx =  new Array(mn,mx,indMn,indMx);
    return(vmnmx);
}

function vMinMaxAbs(list) {
// returns min and max of absolute values of a list's elements
    var mn = Math.abs(list[0]);
    var mx = Math.abs(list[0]);
    var val;
    for (var i=1; i < list.length; i++) {
        val = Math.abs(list[i]);
            if (mn > val) mn = val;
            if (mx < val) mx = val;
    }
    var vmnmx =  new Array(mn,mx);
    return(vmnmx);
}

function randBoolean(p){ // random boolean value, prob p that it is true
    if (typeof(p) == 'undefined' || p == null) {
        p = 0.5;
    }
    if (rand.next() <= p) {
        return(false);
    } else {
        return(true);
    }
}

function sortUnique(list,order) { // sort a list, remove duplicate entries
    var temp = list;
    if (typeof(order) != 'undefined' && order != null) {
        temp.sort(order);
    } else {
        temp.sort();
    }
    var temp2 = new Array(temp.length);
    temp2[0] = temp[0];
    var ix = 0;
    for (var i=1; i < temp.length; i++) {
        if (temp[i] != temp2[ix] ) {
            temp2[++ix] = temp[i];
        }
    }
    return(temp2);
}

function randPermutation(list,index) { // returns a random permutation of list
    var randIndex = listOfDistinctRandInts(list.length,0,list.length-1);
    var thePermutation = new Array(list.length);
    for (var i=0; i < list.length; i++) {
        thePermutation[i] = list[randIndex[i]];
    }
    if (typeof(index) != 'undefined' && index == 'forward') { // original indices
        var p = new Array(2);
        p[0] = thePermutation;
        p[1] = randIndex;
        thePermutation = p;
    } else if (typeof(index) != 'undefined' && index == 'inverse') { // inverse permutation
        var p = new Array(2);
        p[0] = thePermutation;
        p[1] = new Array(list.length);
        for (var i=0; i < list.length; i++) {
            p[1][randIndex[i]] = i;
        }
        thePermutation = p;
    }
    return(thePermutation);
}

function fakeBivariateData(nPoints, funArray, heteroFac, snr, loEnd, hiEnd) {
   // returns a 2-d array of synthetic data generated from a polynomial,
   // according to the contents of funArray.
   // if funArray[0] == "polynomial", uses the other elements of funArray as
   // the coefficients of a polynomial.
   // funArray[1] + funArray[2]*X + funArray[3]*X^2 + ...
   // 1/3 of the points have noise level heteroFac times larger than the rest.
   // Normalizes the errors  to signal/noise ratio snr  in 2-norm
    var data = new Array(2);
    data[0] = new Array(nPoints);
    data[1] = new Array(nPoints);
    var x;
    var fVal;
    var xPow;
    if (funArray[0] == "polynomial") {
        if (typeof(loEnd) == 'undefined' || loEnd == null) {   // lower limit of X variable
            loEnd = -10;
        }
        if (typeof(hiEnd) == 'undefined' || hiEnd == null) {   // upper limit of X variable
            hiEnd = 10;
        }
        var dX = (hiEnd - loEnd)/(nPoints - 1);
        for (var i=0; i < nPoints; i++) {
            x = loEnd + i*dX;
            data[0][i] = x;
            fVal = 0.0;
            xPow = 1.0;
            for (var j=1;  j < funArray.length; j++) {
                fVal +=  xPow*funArray[j];
                xPow *= x;
            }
            data[1][i] = fVal;
        }
    } else {
        alert('Error #1 in irGrade.fakeBivariateData()!\n' +
            'Unsupported function type: ' + funArray[0].toString());
        return(null);
    }
// now add noise.
    var sigNorm = twoNorm(data[1]);
    var noise = new Array(nPoints);
    for (var i=0; i < nPoints; i++) {
        noise[i] = rNorm();
    }
// pick a random set to perturb for heteroscedastic noise
    var segLen = Math.floor(nPoints/3);
    var startPt = Math.floor(2*nPoints/3*rand.next());
    for (var i=startPt; i < startPt+segLen; i++) {
        noise[i] = noise[i]*heteroFac;
    }
    var noiseNorm = twoNorm(noise);
    for (var i=0; i < nPoints; i++) {
        data[1][i] += noise[i]*sigNorm/noiseNorm/snr;
    }
    return(data)
}

function nextRand() {  // generates next random number in a sequence
    var up   = this.seed / this.Q;
    var lo   = this.seed % this.Q;
    var trial = this.A * lo - this.R * up;
    if (trial > 0) {
        this.seed = trial;
    } else {
        this.seed = trial + this.M;
    }
    return (this.seed * this.oneOverM);
}

function rng(s) {
       if ( typeof(s)=='undefined' || s == null ){
           var d = new Date();
           this.seed = 2345678901
             + (d.getSeconds() * 0xFFFFFF)
             + (d.getMinutes() * 0xFFFF);
       } else {
           this.seed = s;
       }
       this.A = 48271;
       this.M = 2147483647;
       this.Q = this.M / this.A;
       this.R = this.M % this.A;
       this.oneOverM = 1.0 / this.M;
       this.next = nextRand;
       this.getSeed = getRandSeed;
       return(this);
}

function getRandSeed() { // get seed of random number generator
    return(this.seed);
}

function crypt(s,t) {
    var slen = s.length;
    var tlen = t.length;
    var rad = 16;
    var r = 0;
    var i;
    var j = -1;
    var result = '';
    if (s.substr(0,2) == "0x") {
        for (i=2; i < slen; i+=2) {
            if (++j >= tlen) {j = 0;}
            r = parseInt(s.substr(i,2),rad) ^ t.charCodeAt(j);
            result += String.fromCharCode(r);
        }
    } else {
        result +='0x';
        for ( i=0; i < slen; i++) {
           if (++j >= tlen) {j = 0;}
           r = s.charCodeAt(i) ^ t.charCodeAt(j);
           result += (r < rad ? "0" : "") + r.toString(rad);
        }
    }
    return(result);
}

