Digging deeper

Understanding the grouping

If you've ever had to do complex calculations or tried to implement a serious algorithm, you may have noticed some of the challenges of doing math with JavaScript. In the case of symbolic math one of those challenges is the inability to do any form of operator overloading. It's far easier to look at and use 4*x^2 then it is to use something like multiply('4', pow('x', '2')). Another downside is that because the primary datatype has to be a string if you're to have anything resembling 4*x^2, your overhead is quite high.

Nerdamer employs a somewhat unorthodox way of representing polynomials, and symbols. It still helps to have an understanding of polynomial rings and domains, and the more conventional way of doing things but for the most part it will be used implicitly.

Nerdamer uses a grouping system and a unified way of representing symbols. Symbols are represented as a multiplier, a value, and a power, {multiplier}*{base}^{power}. The symbols are then grouped into 8 groups.

  1. N: This groups is just a vanilla number.
  2. P: This groups is a number raised to a number power e.g. 2^(3/5). Integer powers are generally converted to N right away.
  3. S: This groups is basically a variable. x, s, y^2
  4. EX: This groups is any symbol raised to a variable power. Basically exponential functions.
  5. FN: This groups is just a function. e.g. cos(x), abs(x)
  6. PL: This groups is any recurring symbol through addition differing only in power e.g. x+x^2, cos(x)+cos(x)^y. If the base is the same they get grouped into this group.
  7. CB: This groups is a multivariate monomial. A group of symbols held together through multiplication x*y*t^2
  8. CP: This groups is similar to PL but the bases are different e.g. r+t, x+1

It's tempting to call the PL group and CP group polynomials but remember they don't discriminate based on power so x^(-1)+x will make the cut. To check if a symbol is a valid polynomial special functions have to be used.

The benefit of this system is that it allows us to leverage JavaScript object lookup rather that loop and search. take this example

Symbol {
    x: {x+x^2},
    y: {y^2}
}

If we are to add a symbol x then the lookup goes something like: Look for x in symbol. If x is found and it's not of group S then it's of group PL and see if this power already exists. If it does, add them otherwise create a new entry within x.

One major benefit of this representation vs and array based representation comes when evaluating polynomials with large powers. Given the polynomial x^5 for instance JavaScript would represent it as

x: [,,,,,1] 

with the zero index being the constants. You can quickly see how a polynomial in the form of

x^10000000000*x^10000000000

can quickly take a browser to its knees vs the group method. The group method has some major inefficiencies mainly when dealing with cases such as polynomial long division, GCD computations, etc. In this case nerdamer switches to the array based representation. At this point we have bigger concerns, mainly integer overflow.

Symbol representation

Symbols are also represented in a simplified uniform matter. At the very least a symbol contains a multiplier, a value, a power, and a group. This is what the number 1 looks like:

{ 
    group: 1,
    value: '#',
    multiplier: 
    { [Number: 1]
        num: { [Number: 1] value: 1, sign: false, isSmall: true },
        den: { [Number: 1] value: 1, sign: false, isSmall: true } 
    },
    power: 
    { [Number: 1]
       num: { [Number: 1] value: 1, sign: false, isSmall: true },
       den: { [Number: 1] value: 1, sign: false, isSmall: true } 
    } 
}
                

The power and multiplier are integer ratios of the bigNumber class. All numbers have a value of '#'. Other symbols carry their variable minus the power and multiplier as their value which is why x^2 can be group with x^3, etc. All symbols have a multiplier of class Frac. All symbols with the exception of group EX have a power of the class Frac as well. EX have a symbol as their power. If udring an operation the symbol's power results in a number, nerdamer will automatically convert the group out of the EX group and the power back to class Frac. The goal is always keep the simplest and most compatible form.

Parsing

To perform the actual parsing of the string, nerdamer uses the shunting-yard algorithm. While parsing several classes are utilized to accommodate functions, vectors, matrices, and special operators. It's also important to note that the nodes get collapsed as soon as possible. Additionally operators return 1 of the existing symbols. This was an early design decision which initially helped speed things up a bit but that may have since changed. The most important thing to remember is that when using symbols in subsequent calculations the clone method should be used to get a clean copy. For example:


//You can interact with the parser directly by getting the core
//with nerdamer loaded either in a web page or node.js
var core = nerdamer.getCore();
//the parser can be accessed in the core through PARSER. 
//Make a shortcut using underscore
var _ = core.PARSER;
//the parser requires objects of class Symbol
var Symbol = core.Symbol;
//create a symbol
var x1 = new Symbol('x');
//one more
var x2 = new Symbol('x');
//add them using the parser
var result = _.add(x1, x2);
//in this case x1 was recycled
console.log(result === x1); //true

This only becomes a concern when directly interacting with Symbols. When parsing strings a fresh Symbol is created each time.

Function handling

I've covered how to set custom functions in Extending the core but that doesn't cover how nerdamer deals with functions. While parsing nerdamer first looks at the PARSER.functions object. This object let's the parser know which functions are available. The entry should contain 2 items. The first being the symbolic handler if any (not all functions have this) and the second is how may arguments to expect. If left blank the parser will first search for that function in the built-in Math object and if it comes up empty it will search in the Math2 object, and object created especially for adding custom functions. The Math2 object should only contain numeric functions since this is the function which will get exported when the buildFunction method is called. Let's look at an example.


//You can interact with the parser directly by getting the core
//with nerdamer loaded either in a web page or node.js
var core = nerdamer.getCore();
//the parser can be accessed in the core through PARSER. 
//Make a shortcut using underscore
var _ = core.PARSER;
//when parsing the function first looks into the built-in Math object
//and then into the Math2 object
//add a custom function
core.Math2.custom = function(a, b) {
    return (2*a+b)/a;
};
//let nerdamer know that it's ok to access this function
//we do that using an array. The first parameter is the special handler
//which we'll leave blank for now. This will only give it numeric capabilities
_.functions.custom = [,2];
//we can now use the function
var x = nerdamer('custom(2, 6)').evaluate();
console.log(x.toString()); //5
//It can't handle symbolics as illustrated next
var y = nerdamer('custom(a, b)').evaluate();
console.log(y.toString()); //custom(a, b)                 

To enable symbolic capabilities we use the above example but provide the definition with a symbol handler.


var core = nerdamer.getCore();
var _ = core.PARSER;
core.Math2.custom = function(a, b) {
    return (2*a+b)/a;
};
//symbolic handler
function symbolicHandler(a, b) {
    //for simplicity we'll work stricly with the Symbol class
    //Remember earlier we spoke of calling clone? In this case the A !== a
    //but it's good practice to call clone on values that are going to be reused
    //If a was to become 2*a then we'd also be dividing by 2*a and not a
    var A = _.multiply(new core.Symbol(2), a.clone());
    var B =_.add(A, b);
    return _.divide(B, a);
};
//let nerdamer know that it's ok to access this function
//we do that using an array. The first parameter is the special handler
//which we'll leave blank for now. This will only give it numeric capabilities
_.functions.custom = [symbolicHandler,2];
//we can now use the function
var x = nerdamer('custom(2, 6)').evaluate();
console.log(x.toString()); //5
//It can't handle symbolics as illustrated next
var y = nerdamer('custom(a, b)').evaluate();
console.log(y.toString()); //(2*a+b)*a^(-1)          

Now in the above example we divide by a which can be zero. Let's imagine for a second, and this is a completely contrived example chosen for simplicity, that division by zero resulted in a imaginary number. We can add a special handler for this in the symbolic handler.


var core = nerdamer.getCore();
var _ = core.PARSER;
core.Math2.custom = function(a, b) {
    return (2*a+b)/a;
};
//symbolic handler
function symbolicHandler(a, b) {
    //we do a check and return a special value for a === 0
    if(a.equals(0)) {
        //the value for imaginary numbers is stored in core.Settings.IMAGINARY
        //this should be used in case the user decides to use something different
        //like j for example
        return new core.Symbol(core.Settings.IMAGINARY);
    }
    //for simplicity we'll work stricly with the Symbol class
    //Remember earlier we spoke of calling clone? In this case the A !== a
    //but it's good practice to call clone on values that are going to be reused
    //If a was to become 2*a then we'd also be dividing by 2*a and not a
    var A = _.multiply(new core.Symbol(2), a.clone());
    var B =_.add(A, b);
    return _.divide(B, a);
};
//let nerdamer know that it's ok to access this function
//we do that using an array. The first parameter is the special handler
//which we'll leave blank for now. This will only give it numeric capabilities
_.functions.custom = [symbolicHandler,2];
//we can now use the function
var x = nerdamer('custom(0, 2)').evaluate();
console.log(x.toString()); //i
var y = nerdamer('custom(9, 3)').evaluate();
console.log(y.toString());//7/3
var z = nerdamer('custom(x, y)');
console.log(z.toString());//(2*x+y)*x^(-1)             

It might be apparent to you by now that you have no commitment to any of the built-in functions and you are completely at liberty to override any or all of them. If you have another library who's version of cos you like, you can just override it using the above example and nerdamer will gladly use that one instead. This gives you the option of using nerdamer as a purely symbolic layer.

Custom operators

You can set custom operators to a degree. Below is an example of how to accomplish this.


var core = nerdamer.getCore();
var _ = core.PARSER;
//see core source code for full explanation or parameters. In this case just know
//The first param is the operator character so the parser can recognize it
//The second is the name of the parser function that it maps to
//The third is the order in which to parse first. e.g. an order of 5 will get parsed before 4.
//The fourth is if it's left associative. 
//The fifth is if it's a prefix operator
//And the last is if it's a postfix operator
var Operator = core.Operator;
//create a blank operator so the operator knows it's an operator 
//because the parser only sees one character at a time. We don't define anything in this
//case since it's a dummy operator to let nerdamer know that it should be treated as one.
_.operators['<'] = new Operator('<');
//create the operator you want and we'll name it bitleft. It's up to you. Just make
//sure you call it the same as the function defined below. 
_.operators['<<'] = new Operator('<<', 'bitleft', 1, true, false);
//define the function
_.bitleft = function(a, b) {
    return a << b;
}; 
//we can now use it.
var x = nerdamer('5<<2');
console.log(x.toString()); //20               

More coming soon ...