simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

custom_argument_parsing.js (3031B)


      1 /**
      2  * The expression parser of math.js has support for letting functions
      3  * parse and evaluate arguments themselves, instead of calling them with
      4  * evaluated arguments.
      5  *
      6  * By adding a property `raw` with value true to a function, the function
      7  * will be invoked with unevaluated arguments, allowing the function
      8  * to process the arguments in a customized way.
      9  */
     10 const { create, all } = require('../..')
     11 const math = create(all)
     12 
     13 /**
     14  * Calculate the numeric integration of a function
     15  * @param {Function} f
     16  * @param {number} start
     17  * @param {number} end
     18  * @param {number} [step=0.01]
     19  */
     20 function integrate (f, start, end, step) {
     21   let total = 0
     22   step = step || 0.01
     23   for (let x = start; x < end; x += step) {
     24     total += f(x + step / 2) * step
     25   }
     26   return total
     27 }
     28 
     29 /**
     30  * A transformation for the integrate function. This transformation will be
     31  * invoked when the function is used via the expression parser of math.js.
     32  *
     33  * Syntax:
     34  *
     35  *     integrate(integrand, variable, start, end)
     36  *     integrate(integrand, variable, start, end, step)
     37  *
     38  * Usage:
     39  *
     40  *     math.evaluate('integrate(2*x, x, 0, 2)')
     41  *     math.evaluate('integrate(2*x, x, 0, 2, 0.01)')
     42  *
     43  * @param {Array.<math.Node>} args
     44  *            Expects the following arguments: [f, x, start, end, step]
     45  * @param {Object} math
     46  * @param {Object} [scope]
     47  */
     48 integrate.transform = function (args, math, scope) {
     49   // determine the variable name
     50   if (!args[1].isSymbolNode) {
     51     throw new Error('Second argument must be a symbol')
     52   }
     53   const variable = args[1].name
     54 
     55   // evaluate start, end, and step
     56   const start = args[2].compile().evaluate(scope)
     57   const end = args[3].compile().evaluate(scope)
     58   const step = args[4] && args[4].compile().evaluate(scope) // step is optional
     59 
     60   // create a new scope, linked to the provided scope. We use this new scope
     61   // to apply the variable.
     62   const fnScope = Object.create(scope)
     63 
     64   // construct a function which evaluates the first parameter f after applying
     65   // a value for parameter x.
     66   const fnCode = args[0].compile()
     67   const f = function (x) {
     68     fnScope[variable] = x
     69     return fnCode.evaluate(fnScope)
     70   }
     71 
     72   // execute the integration
     73   return integrate(f, start, end, step)
     74 }
     75 
     76 // mark the transform function with a "rawArgs" property, so it will be called
     77 // with uncompiled, unevaluated arguments.
     78 integrate.transform.rawArgs = true
     79 
     80 // import the function into math.js. Raw functions must be imported in the
     81 // math namespace, they can't be used via `evaluate(scope)`.
     82 math.import({
     83   integrate: integrate
     84 })
     85 
     86 // use the function in JavaScript
     87 function f (x) {
     88   return math.pow(x, 0.5)
     89 }
     90 console.log(math.integrate(f, 0, 1)) // outputs 0.6667254718034714
     91 
     92 // use the function via the expression parser
     93 console.log(math.evaluate('integrate(x^0.5, x, 0, 1)')) // outputs 0.6667254718034714
     94 
     95 // use the function via the expression parser (2)
     96 const scope = {}
     97 math.evaluate('f(x) = 2 * x', scope)
     98 console.log(math.evaluate('integrate(f(x), x, 0, 2)', scope)) // outputs 4.000000000000003