UNPKG

btm-expressions

Version:

BTM (bowtie-math) is a math object model to enable parsing of mathematical expressions into a tree structure that can be manipulated, evaluated, and compared.

627 lines (603 loc) 24.2 kB
/*! * BTM JavaScript Library v@VERSION * https://github.com/dbrianwalton/BTM * * Copyright D. Brian Walton * Released under the MIT license (https://opensource.org/licenses/MIT) * * Date: @DATE */ /* *************************************************** * Define the Function Expression -- defined by a function name and input expression * *************************************************** */ import { expression } from "./expression.js" import { create_scalar } from "./scalar_expr.js" import { variable_expr } from "./variable_expr.js" import { unop_expr } from "./unop_expr.js" import { binop_expr } from "./binop_expr.js" import { exprType } from "./BTM_root.js" export class function_expr extends expression { constructor(menv, name, inputExpr, restrictDomain) { super(menv); this.type = exprType.fcn; // Count how many derivatives. var primePos = name.indexOf("'"); this.derivs = 0; if (primePos > 0) { this.name = name.slice(0,primePos); this.derivs = name.slice(primePos).length; } else { this.name = name; } if (typeof inputExpr == 'undefined') inputExpr = new expression(); this.inputs = [inputExpr]; inputExpr.parent = this; this.domain = restrictDomain; this.alternate = null; this.builtin = true; switch(this.name) { case 'asin': case 'acos': case 'atan': case 'asec': case 'acsc': case 'acot': this.name = 'arc'+this.name.slice(1,4); break; case 'log': this.name = 'ln'; break; case 'sin': case 'cos': case 'tan': case 'csc': case 'sec': case 'cot': case 'arcsin': case 'arccos': case 'arctan': case 'arcsec': case 'arccsc': case 'arccot': case 'sqrt': case 'root': case 'abs': case 'exp': case 'expb': case 'ln': case 'log10': break; default: this.builtin = false; break; } // If using a derivative of a known function, then we should compute that in advance. if (this.builtin && this.derivs > 0) { var xvar = new variable_expr(this.menv, "x"); var deriv = new function_expr(this.menv, this.name, xvar); for (var i=0; i<this.derivs; i++) { deriv = deriv.derivative(xvar, {"x":0}); } var binding = {}; binding["x"] = inputExpr; this.alternate = deriv.compose(binding); } } getName() { return (this.name + "'".repeat(this.derivs)); } toString(elementOnly) { var fcnString, retString; if (typeof elementOnly == 'undefined') { elementOnly = false; } fcnString = this.getName(); if (elementOnly) { retString = fcnString; } else { var argStrings = []; if (this.inputs.length == 0 || typeof this.inputs[0] == 'undefined') { argStrings.push('?'); } else { for (var i in this.inputs) { argStrings.push(this.inputs[i].toString()); } } retString = fcnString + '(' + argStrings.join(',') + ')'; } return(retString); } // Return an array containing all tested equivalent strings. allStringEquivs() { var allInputs = [], inputOptions = []; for (var i in this.inputs) { inputOptions.push(this.inputs[i].allStringEquivs()); } var retValue = []; var fcnString = this.getName(); // Want to create a list of all possible input representations. function generateArgs(left, rightOptions) { if (rightOptions.length==0) { allInputs.push(left); } else { var N = left.length; var newLeft = []; for (var k in left) { newLeft.push(left[k]); } for (var k in rightOptions[0]) { newLeft[N] = rightOptions[0][k]; generateArgs(newLeft, rightOptions.slice(1)); } } } generateArgs([], inputOptions); for (var i in allInputs) { retValue[i] = fcnString+'(' + allInputs[i].join('+') + ')'; } return(retValue); } toTeX(showSelect) { if (typeof showSelect == 'undefined') { showSelect = false; } var texString = ''; var fcnString; var argStrings = []; if (typeof this.inputs[0] == 'undefined') { argStrings.push('?'); } else { for (var i in this.inputs) { argStrings.push(this.inputs[i].toTeX(showSelect)); if (showSelect && this.select) { argStrings[i] = "{\\color{blue}" + argStrings[i] + "}"; } } } switch(this.name) { case 'sin': fcnString = '\\sin'; break; case 'cos': fcnString = '\\cos'; break; case 'tan': fcnString = '\\tan'; break; case 'csc': fcnString = '\\csc'; break; case 'sec': fcnString = '\\sec'; break; case 'cot': fcnString = '\\cot'; break; case 'arcsin': fcnString = '\\sin^{-1}'; break; case 'arccos': fcnString = '\\cos^{-1}'; break; case 'arctan': fcnString = '\\tan^{-1}'; break; case 'arccsc': fcnString = '\\csc^{-1}'; break; case 'arcsec': fcnString = '\\sec^{-1}'; break; case 'arccot': fcnString = '\\cot^{-1}'; break; case 'sqrt': fcnString = '\\mathrm{sqrt}'; texString = '\\sqrt{' + argStrings[0] + '}'; break; case 'root': fcnString = '\\mathrm{root}'; texString = '\\sqrt[' + argStrings[1] +']{' + argStrings[0] + '}'; break; case 'abs': fcnString = '\\abs'; texString = '\\left|' + argStrings[0] + '\\right|'; break; case 'exp': fcnString = 'e^'; texString = 'e^{' + argStrings[0] + '}'; break; case 'expb': fcnString = '\\exp'; break; case 'ln': fcnString = '\\ln' break; case 'log10': fcnString = '\\log_{10}' break; default: if (this.name.length > 1) { fcnString = '\\mathrm{' + this.name + '}'; } else { fcnString = this.name; } break; } if (this.derivs > 0) { if (this.derivs <= 3) { fcnString = fcnString + "'".repeat(this.derivs); } else { fcnString = fcnString + "^{("+this.derivs+")}"; } } if (showSelect && this.select) { fcnString = "\\color{red}{" + fcnString + "}"; texString = ''; } if (texString == '') { texString = fcnString + ' \\mathopen{}\\left(' + argStrings.join(',') + '\\right)\\mathclose{}'; } return(texString); } toMathML() { var texString; var argString; if (typeof this.inputs[0] == 'undefined') { argString = '?'; } else { argString = this.inputs[0].toMathML(); } switch(this.name) { case 'sin': case 'cos': case 'tan': case 'csc': case 'sec': case 'cot': case 'arcsin': case 'arccos': case 'arctan': case 'exp': case 'expb': case 'ln': case 'abs': texString = '<apply><' + this.name + '/>' + argString + '</apply>'; break; case 'sqrt': texString = '<apply><root/>' + argString + '</apply>'; break; case 'log10': texString = '<apply><log/><logbase><cn>10</cn></logbase>' + argString + '</apply>'; break; default: texString = '<apply><ci>' + this.name + '</ci>' + argString + '</apply>'; break; } return(texString); } operateToTeX() { var fcnString; switch(this.name) { case 'sin': fcnString = '\\sin'; break; case 'cos': fcnString = '\\cos'; break; case 'tan': fcnString = '\\tan'; break; case 'csc': fcnString = '\\csc'; break; case 'sec': fcnString = '\\sec'; break; case 'cot': fcnString = '\\cot'; break; case 'arcsin': fcnString = '\\sin^{-1}'; break; case 'arccos': fcnString = '\\cos^{-1}'; break; case 'arctan': fcnString = '\\tan^{-1}'; break; case 'arccsc': fcnString = '\\csc^{-1}'; break; case 'arcsec': fcnString = '\\sec^{-1}'; break; case 'arccot': fcnString = '\\cot^{-1}'; break; case 'sqrt': fcnString = '\\mathrm{sqrt}'; break; case 'abs': fcnString = '\\abs'; break; case 'exp': case 'expb': fcnString = '\\exp'; break; case 'ln': fcnString = '\\ln' break; case 'log10': fcnString = '\\log_{10}' break; default: if (this.name.length > 1) { fcnString = '\\mathrm{' + this.name + '}'; } else { fcnString = this.name; } break; } if (this.derivs > 0) { if (this.derivs <= 3) { fcnString = fcnString + "'".repeat(this.derivs); } else { fcnString = fcnString + "^{("+this.derivs+")}"; } } return(fcnString+"(\\Box)"); } evaluate(bindings) { var inputVal = this.inputs[0].evaluate(bindings); var retVal = undefined; if (inputVal == undefined) { return(undefined); } // Built-in functions with derivatives have computed derivative earlier. if (this.builtin && this.derivs > 0) { if (this.alternate != undefined) { retVal = this.alternate.evaluate(bindings); } else { console.log("Error: Built-in function called with unspecified derivative formula."); } } else { if (bindings[this.name] == undefined) { // Check the list of common mathematical functions. switch(this.name) { case 'sin': retVal = Math.sin(inputVal); break; case 'cos': retVal = Math.cos(inputVal); break; case 'tan': retVal = Math.tan(inputVal); break; case 'csc': retVal = 1/Math.sin(inputVal); break; case 'sec': retVal = 1/Math.cos(inputVal); break; case 'cot': retVal = 1/Math.tan(inputVal); break; case 'arcsin': if (Math.abs(inputVal) <= 1) { retVal = Math.asin(inputVal); } break; case 'arccos': if (Math.abs(inputVal) <= 1) { retVal = Math.acos(inputVal); } break; case 'arctan': retVal = Math.atan(inputVal); break; case 'arccsc': if (Math.abs(inputVal) >= 1) { retVal = Math.asin(1/inputVal); } break; case 'arcsec': if (Math.abs(inputVal) >= 1) { retVal = Math.acos(1/inputVal); } break; case 'arccot': if (inputVal == 0) { retVal = Math.PI/2; } else { retVal = Math.PI/2 - Math.atan(1/inputVal); } break; case 'sqrt': if (inputVal >= 0) { retVal = Math.sqrt(inputVal); } break; case 'abs': retVal = Math.abs(inputVal); break; case 'exp': case 'expb': retVal = Math.exp(inputVal); break; case 'ln': if (inputVal > 0) { retVal = Math.log(inputVal); } break; case 'log10': if (inputVal > 0) { retVal = Math.LOG10E * Math.log(inputVal); } break; default: // See if we have already used this function. // For consistency, we should keep it the same. var functionEntry = this.menv.functions[this.name]; // If never used previously, generate a random function. // This will allow valid comparisons to occur. if (functionEntry == undefined) { console.log("Error: A custom function never had a backend definition."); } // Copy the bindings. var fBind = {}; Object.keys(bindings).forEach(function(key) { fBind[ key ] = bindings[ key ]; }); // Now, use the variable of the function. var inVar = functionEntry["input"]; if (Array.isArray(inVar)) { console.log("Error: Function is defined to expect multiple inputs. Not yet implemented."); } fBind[inVar] = inputVal; // See if we need additional derivatives in binding if (this.derivs >= functionEntry["value"].length) { var ivar = new variable_expr(this.menv, inVar); var varBind = {}; varBind[ivar] = 0; for (var i=functionEntry["value"].length; i <= this.derivs; i++) { functionEntry["value"][i] = functionEntry["value"][i-1].derivative(ivar, varBind); } } retVal = functionEntry["value"][this.derivs].evaluate(fBind); break; } } else { var functionEntry = bindings[this.name]; // Copy the bindings. var fBind = {}; Object.keys(bindings).forEach(function(key) { fBind[ key ] = bindings[ key ]; }); // Now, use the variable of the function. var inVar = functionEntry["input"]; if (Array.isArray(inVar)) { console.log("Error: Function is defined to expect multiple inputs. Not yet implemented."); } fBind[inVar] = inputVal; // See if we need additional derivatives in binding if (this.derivs >= functionEntry["value"].length) { var ivar = new variable_expr(this.menv, inVar); var varBind = {}; varBind[ivar] = 0; for (var i=functionEntry["value"].length; i <= this.derivs; i++) { functionEntry["value"][i] = functionEntry["value"][i-1].derivative(ivar, varBind); } } retVal = functionEntry["value"][this.derivs].evaluate(fBind); } } return(retVal); } flatten() { return(new function_expr(this.menv, this.getName(), this.inputs[0].flatten())); } copy() { return(new function_expr(this.menv, this.getName(), this.inputs[0].copy())); } compose(bindings) { return(new function_expr(this.menv, this.getName(), this.inputs[0].compose(bindings))); } derivative(ivar, varList) { var theDeriv; var depArray = this.inputs[0].dependencies(); var uConst = true; var ivarName = (typeof ivar == 'string') ? ivar : ivar.name; for (var i=0; i<depArray.length; i++) { if (depArray[i] == ivarName) { uConst = false; } } if (uConst) { theDeriv = create_scalar(this.menv, 0); } else { var dydu; switch(this.name) { case 'sin': dydu = new function_expr(this.menv, 'cos', this.inputs[0]); break; case 'cos': dydu = new unop_expr(this.menv, '-', new function_expr(this.menv, 'sin', this.inputs[0])); break; case 'tan': var theSec = new function_expr(this.menv, 'sec', this.inputs[0]); dydu = new binop_expr(this.menv, '^', theSec, create_scalar(this.menv, 2)); break; case 'csc': var theCot = new function_expr(this.menv, 'cot', this.inputs[0]); dydu = new unop_expr(this.menv, '-', new binop_expr(this.menv, '*', this, theCot)); break; case 'sec': var theTan = new function_expr(this.menv, 'tan', this.inputs[0]); dydu = new binop_expr(this.menv, '*', this, theTan); break; case 'cot': var theCsc = new function_expr(this.menv, 'csc', this.inputs[0]); dydu = new unop_expr(this.menv, '-', new binop_expr(this.menv, '^', theCsc, create_scalar(this.menv, 2))); break; case 'arcsin': var theCos = new binop_expr(this.menv, '-', create_scalar(this.menv, 1), new binop_expr(this.menv, '^', this.inputs[0], create_scalar(this.menv, 2))); dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, 1), new function_expr(this.menv, 'sqrt', theCos)); break; case 'arccos': var theSin = new binop_expr(this.menv, '-', create_scalar(this.menv, 1), new binop_expr(this.menv, '^', this.inputs[0], create_scalar(this.menv, 2))); dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, -1), new function_expr(this.menv, 'sqrt', theSin)); break; case 'arctan': var tanSq = new binop_expr(this.menv, '^', this.inputs[0], create_scalar(this.menv, 2)); dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, 1), new binop_expr(this.menv, '+', create_scalar(this.menv, 1), tanSq)); break; case 'arcsec': var theSq = new binop_expr(this.menv, '^', this.inputs[0], create_scalar(this.menv, 2)); var theRad = new binop_expr(this.menv, '-', theSq, create_scalar(this.menv, 1)); dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, 1), new binop_expr(this.menv, '*', new function_expr(this.menv, 'abs', this.inputs[0]), new function_expr(this.menv, 'sqrt', theRad))); break; case 'arccsc': var theSq = new binop_expr(this.menv, '^', this.inputs[0], create_scalar(this.menv, 2)); var theRad = new binop_expr(this.menv, '-', theSq, create_scalar(this.menv, 1)); dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, -1), new binop_expr(this.menv, '*', new function_expr(this.menv, 'abs', this.inputs[0]), new function_expr(this.menv, 'sqrt', theRad))); break; case 'arccot': var cotSq = new binop_expr(this.menv, '^', this.inputs[0], create_scalar(this.menv, 2)); dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, -1), new binop_expr(this.menv, '+', create_scalar(this.menv, 1), cotSq)); break; case 'sqrt': dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, 1), new binop_expr(this.menv, '*', create_scalar(this.menv, 2), this)); break; case 'abs': dydu = new binop_expr(this.menv, '/', this, this.inputs[0]); break; case 'exp': case 'expb': dydu = new function_expr(this.menv, this.name, this.inputs[0]); break; case 'ln': dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, 1), this.inputs[0]); break; case 'log10': dydu = new binop_expr(this.menv, '/', create_scalar(this.menv, Math.LOG10E), this.inputs[0]); break; default: dydu = new function_expr(this.menv, this.getName()+"'", this.inputs[0]); break; } if (!uConst && this.inputs[0].type == exprType.variable) { theDeriv = dydu; } else { var dudx = this.inputs[0].derivative(ivar, varList); if (dudx == undefined) { theDeriv = undefined; } else { theDeriv = new binop_expr(this.menv, '*', dydu, dudx); } } } return(theDeriv); } }