CAS: how to write user functions / extend it

Discussions about extensions and frameworks

CAS: how to write user functions / extend it

Postby oliver » Sat Jul 28, 2012 3:02 pm

The CAS of ND1 employs Mathematica™ in the cloud via Wolfram|Alpha™ commercial API.

If you're familiar with using wolframalpha.com or Mathematica, you can easily write your own functions that use Mathematica or W|A. You can also extend the ND1 CAS, making your functions available from all folders. You may also submit your functions for inclusion in the stock ND1 CAS. They'll be gladly accepted.

To write such functions, you don't have to know how to use the W|A API. Instead, you have to know how to accomplish what you want to do on the W|A command line, and then you write a simple "wrapper" function that use a special calculator function, calculator.callWA(), that will communicate with W|A.

You use JavaScript to use this call. In RPL+, three commands (evalWA, WA, WAopt) are available that serve the same purpose. (That is, you can write wrapper functions in that language, too. See the section W|A family commands in CAS (beta): usage notes http://forums.naivedesign.com/viewtopic.php?f=11&t=663.)

The general form of a user function that uses W|A looks like this:
Code: Select all
function(arg) {
    /* perform argument checking and/or make arg look as required by W|A  */
    var options = { optionname: value }; /* set query/formatting options */
    var res = calculator.callWA("command", arg, options);
   /* filter res for the information you're interested in */
   return res;
}


Regularly, this simplifies to a single line like this:
Code: Select all
function(x) { return calculator.callWA("simplify", x); };


The equivalent in RPL+:
Code: Select all
≪ "simplify" WA ≫


The callWA(command, arg, options) (RPL+: WAopt) function takes the following arguments:
  • command (String): a W|A or Mathematica command/function name
  • arg (any ND1 type or vector thereof): the argument(s) for the command to operate on
  • options (Object): a dictionary of supported option key and values

command and arg together will form the query. The type in arg (which currently may be any of: Algebraic Expression, Real, Complex number, BigInt, BigFloat, or an Array/Vector/Matrix containing any of those types) will be translated from its ND1 representation into what W|A expects for it. For example, a Real simply becomes a number string, but a Complex number (3,4) will be reformatted to look like 3+4i. A vector [3 4 5] will become {3,4,5}, etc.

The call will submit the query, wait for a response, and convert it from its W|A representation into the appropriate ND1 type(s). options gives you an optional way to influence how the query is formed, and how the response is processed.

You need to know what you have to type on the wolframalpha.com command line to have it do the computation you're after.
Typing in Mathematica code is an excellent way to make sure W|A will return the expected result. W|A also has strong natural language interpretation, and you may indeed go with any string that will work on the W|A command line as your command arg to callWA(). But, a natural language query may not exactly do what you want it to, or may produce the desired result, but not as the primary result.

Run without specifying options, the callWA() call will only request the textual contents of the primary result from W|A. That is, the API's "Result" pod. This is the name of a certain field in the result from W|A. It's a detail from the W|A query API, and the one thing to understand about the API.

It's actually quite simple.
Go to http://products.wolframalpha.com/api/explorer.html. This is a tool that permits you to see what the W|A API is actually returning for a given query.

Type 3+4 and submit.
The result, "7", appears in a plaintext tag under a pod with id Result.

Result is the usual pod name for primary results, and is the implied default, if you don't specify options in your call to callWA().

Type integrate x^2 and submit.
Now the result appears in a pod with id IndefiniteIntegral. The result in plaintext is "integral x^2 dx = x^3/3+constant".

In order to write a function that produces the desired output, 'x^3/3', from this, you'd write a wrapper function like this:
Code: Select all
    function(x) { return calculator.callWA("integrate", x, { "pod": "IndefiniteIntegral", "right": true }); }


In RPL+:
Code: Select all
≪ "integrate" { "pod": "IndefiniteIntegral", "right": true } WAopt ≫


The pod IDs are not documented as part of the W|A API and are a bit arbitrary. As a general rule: use the API explorer to figure out the pod id, if your first attempt at callWA() without specifying a pod fails. In 80% of all cases, the pod id is Result and you don't have to worry about this. If you know the Mathematica code to accomplish something, it's even more certain the result will be in Result.

Here're the supported keys for options:
  • "pod": (String) name of the output pod to request from W|A, if other than Result; the plaintext fields of that pod will be regarded as containing the processing result; multiple results will be collected into a vector
  • "join": (Boolean) in case of a vector arg to callWA(): unroll into list of items; that is, an arg of [a, b, 1] would transform to "a, b, 1" instead of "{a, b, 1}"
  • "expr": (Boolean) process W|A result as an expression; do this whenever you expect that W|A will return an algebraic expression, or vector of expressions
  • "right": (Boolean) choose the right side in returned expression; implies "expr"
  • "rright": (Boolean) choose the right side of a returned result (the left side may contain comments)
  • "approx": (Boolean) choose the right side of an approximated result like "x ~~ 20", instead of stripping it and returning the left side (the default behavior)
  • "erase": (String) or (Regexp) text to remove from results (apply to each component, in case of vector or matrix result)
  • "vec2elem": (Boolean) change one-element result vectors into their sole element; that is a result [y+2] becomes y+2
  • "suffix": (String) text to add to arguments (rarely used)
  • "image": (Boolean) request image results instead of plaintext; the return value will be a URL that can be displayed with display.showImageFromURL() (see the Plot example below)
  • "and": (Boolean) keep and texts in W|A results instead of breaking information right and left from them into vector components
  • "brackets": (String of two characters) text characters to use as vector brackets; default is "{}"
  • "retry": (Function) function to call if W|A returns no pods but a recalculate URL, that will take the original URL and return a new URL to use instead of the W|A-provided recalculate URL
  • "show": (Boolean) debug option: see the W|A query and raw results

Finally, there's one special rule: If you have a Mathematica command, append the opening bracket to indicate to callWA() that this is a Mathmatica command (that is, your command will look something like "Derivative[") and enable specific processing (and automatically provide the closing bracket for you). See the examples for Curl[], D[] and N[] below.

To give you a feel how this looks in practice, here're a number of stock function implementations that show argument type checking, and use of options:
Code: Select all
   "chebychev": function(order) { return calculator.callWA("Chebyshev polynomial", order); },

   "simplify": function(x) { return calculator.callWA("simplify", x, { "expr": true }); },

   "taylor": function(x, name, order) { return calculator.callWA("taylor series of degree " + order + " for", x, { "pod": "SeriesExpansionAtX = 0", "erase": /[+]O.+$/ }); },

   "IABCUV": function(a, b, c) { var res = calculator.callWA("solve", a + "*a+" + b + "*b=" + c + " for integers", { "right": true }); res.length = 2; return res; },

   "ICHINREM": function(x, y) { return this["ChineseRemainder"](ME.matrix.transpose(ME.vector.augment(x, y))); },
   "ChineseRemainder": function(x) { return calculator.callWA("ChineseRemainder", x); },

   "Plot": function(x, args) {
      if (!(calculator.isAVector(x) || calculator.isALikelyExpressionOrName(x)))
         throw Error("wrong type of argument");
      if (!((typeof args[1] == "number" || calculator.isALikelyExpressionOrName(args[1])) && (typeof args[2] == "number" || calculator.isALikelyExpressionOrName(args[2]))))
         throw Error("bad arg");
      var imageURL = calculator.callWA("Plot[", [x, args], { "join": true, "pod": "Plot", "image": true });
      if (imageURL)
         display.showImageFromURL(imageURL);
   },

   "Series": function(x, args) {
      if (calculator.typeOf(x) != "expression or name")
         throw Error("wrong type of argument");
      if (!(typeof args[1] == "number" && typeof args[2] == "number"))
         throw Error("bad arg");
      return calculator.callWA("Series[", [x, args], { "join": true, "expr": true, "pod": "SeriesExpansionAtX = " + args[1], "erase": /[+]O.+$/ });
   },

   "Curl": function(x) { return calculator.callWA("Curl[", x, { "pod": "VectorAnalysisResult", "right": true, "rright": true } ); },

   "Derivative": function(x, varnames) { varnames = varnames || calculator.quote(calculator.independentInExpression(x));
      if (!calculator.isAVector(varnames) && !calculator.isAName(varnames)) throw Error("wrong type of argument");
      return calculator.callWA("D[", [x, [varnames]], { "join": true, "pod": "Input", "rright": true, "expr": true });
   },

   "N": function(name, n) { if (!(typeof n == "number" && n > 0)) throw Error("bad arg"); return BigFloat.fromString(calculator.callWA("N[", [name, n], { "join": true })); },

   "FresnelS": function(x) { return parseFloat(calculator.callWA("FresnelS[", Number(x).toPrecision(15))); },


As with any function you write in ND1, your resulting function will work as command on the edit line, and as function in expressions, in RPL, and in JavaScript.

If you're running into trouble, please use this thread to report, or send me a PM. Thank you!


Injections

If you want your function to work globally (that is, outside a folder), you need to inject it into the calculator engine.

The following describes how to do this.

There'll shortly be a new shared folder, UserCAS, that will allow you to add your function to a list of injections with minimal work.
If your function is a usual CAS command, you may also send it to me (or append to this thread), and I'll be more than happy to add it in the right place to the stock CAS.

Sticking with the example of the simplify function, here's how injection code would look for this function.
Code: Select all
   ME.expr["simplify"] = function(x) { return calculator.callWA("simplify", x); };


(As explained in the JavaScript API document (http://naivedesign.com/ND1/ND1_Reference__JavaScript_API.html), ME stands for MorphEngine, ND1's and CalcPad's calculator engine, and is an alias for calculator.functions. This is a JS object which has various sub-objects, each representing a "function collection". ME.expr is the collection of functions that take expressions as arguments. This will be the most usual case for CAS functions. If your function operates on a Real, you'd add to ME. If it operates on a complex number, you'd add to ME.complex. If it operates on a matrix, you'd add to ME.matrix. Etc.)

[BETA-PERIOD IMPORTANT NOTE: only after the next update will ND1 automatically invoke functions you add to ME.expr (which is a new function collection created for the CAS). If you want to inject code that operates on expressions today, read the following:
Inject into ME and precede your function name with a @ character; that is
Code: Select all
   ME["@simplify"] = function(x) { return calculator.callWA("simplify", x); };

Then, create an alias with the intended name:
Code: Select all
   calculator.function_aliases["simplify"] = "@simplify";

The "@" disables symbolic manipulation on the given function. Without it, ND1 would do something like 'simplify(x^2)' instead of invoking simplify on 'x^2'. (The UserCAS injection template will create the alias for you automatically.)

There's two ways to do the actual injection:
Manually: Enter code like this on the Definition | Injection page (enable JS Injections in the Settings app under ND1, to see this entry on the Definition page).
Automatically: create a Code object (JavaScript code starting with a comment "/*") and use the inject command.

(More details about this can be found in the JavaScript API documentation. Extensions like BigInt and Constants use this and can serve as examples.)
oliver
Site Admin
 
Posts: 433
Joined: Sat May 01, 2010 2:11 pm

Re: CAS: how to write user functions / extend it

Postby oliver » Mon Aug 13, 2012 9:02 am

Updated to include additions in 1.0beta3.
oliver
Site Admin
 
Posts: 433
Joined: Sat May 01, 2010 2:11 pm


Return to Extensions

Who is online

Users browsing this forum: No registered users and 1 guest

cron