├── .gitignore ├── index.js ├── lib ├── index.js └── integral.js ├── package.json ├── LICENSE ├── README.md └── test └── integral.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | node_modules 3 | *.log 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./lib') 3 | ] 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./integral.js') 3 | ]; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mathjs-simple-integral", 3 | "version": "0.1.1", 4 | "description": "Extends mathjs to be able to compute simple integrals.", 5 | "main": "index.js", 6 | "author": "Joel Hoover ", 7 | "license": "Apache-2.0", 8 | "private": false, 9 | "scripts": { 10 | "test": "mocha", 11 | "test:watch": "mocha --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/joelhoover/mathjs-simple-integral" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/joelhoover/mathjs-simple-integral/issues" 19 | }, 20 | "keywords": [ 21 | "math", 22 | "mathjs", 23 | "integral" 24 | ], 25 | "devDependencies": { 26 | "chai": "^4.1.2", 27 | "mocha": "^4.1.0" 28 | }, 29 | "dependencies": { 30 | "mathjs": "^3.18.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Integral for Math.js 2 | This library is an extension for the popular [Math.js](http://mathjs.org) library. It adds an `integral` function that is capable of finding the integrals of simple mathematical functions. It is also flexible in the integration rules used, allowing custom rules to be be used instead of or in addition to the standards rules. 3 | 4 | Note that this software is still considered beta, so if you come across any bug or rough edges, please submit an issue! 5 | 6 | 7 | ## Install 8 | To install the package, use the following: 9 | ``` 10 | npm install mathjs-simple-integral 11 | ``` 12 | 13 | To register the extension with Math.js, use the following: 14 | ```javascript 15 | math.import(require('mathjs-simple-integral')); 16 | ``` 17 | This will add the function `math.integral` to Math.js. 18 | 19 | 20 | ## Usage 21 | The basic usage of this extension is very easy: just provide the integrand (either as a `Node` or a string) and the variable of integration (either as a `SymbolNode` or a string): 22 | 23 | ```javascript 24 | math.integral('x^2', 'x'); // 'x ^ 3 / 3' 25 | math.integral('1/x', 'x'); // 'log(abs(x))' 26 | math.integral('e^x', 'x'); // 'e^x' 27 | math.integral('cos(2*x+pi/6)', 'x'); // 'sin(2 * x + pi / 6) / 2' 28 | ``` 29 | 30 | If `integral` is unable to find the integral of the given expression, it will throw an error: 31 | ```javascript 32 | math.integral('e^(x^2)'); // Error: Unable to find integral of "e ^ (x ^ 2)" with respect to "x" 33 | ``` 34 | 35 | 36 | ### Simplification 37 | By default, `integral` runs `math.simplify` on the output, as the integration process can produce some unwieldy expressions. However, it is possible to get raw, unsimplified output from `integral` by passing in an `options` object with `simplify` set to `false` to the optional third parameter: 38 | ```javascript 39 | math.integral('x^2', 'x', {simplify: false}); // '1 / (2 + 1) * x ^ (2 + 1)' 40 | math.integral('1/(2*x)', 'x', {simplify: false}); // '2 ^ -1 * log(abs(x))' 41 | ``` 42 | 43 | 44 | ### Custom Rules 45 | In this implementation, integration rules are defined as a function that takes as parameters (1) `expr`, the expression to be integrated; (2) `context`, the context of integration; and (3) `subIntegral`, a function that tries to integrate a subexpression or rewritten form of the integral. The integration rule then returns the computed integral, or `null` if it was unable to find one. In addition to many standard integration already implemented (located at `math.integral.rules`), a custom set of integration rules can be specified. 46 | 47 | For example, suppose we added a custom function `myUpperGamma` representing the [Upper Incomplete Gamma Function](https://en.wikipedia.org/wiki/Incomplete_gamma_function), and we now want to add support for integrating it, particularly we want to implement this rule: `integral("myUpperGamma(s,x)", "x") = x*myUpperGamma(s,x) - myUpperGamma(s+1, x)` ([verify here]()). First, let us write this rule as a function: 48 | 49 | ```javascript 50 | var myUpperGammaRule = function(expr, context, subIntegral) { 51 | // Ensure we are trying to integrate a FunctionNode 52 | if(expr.type === "FunctionNode") { 53 | // Ensure we are trying to integrate 'myUpperGamma' and that it has 2 arguments 54 | if(expr.name === "myUpperGamma" && expr.args.length === 2) { 55 | // Ensure that the first argument is constant and the second one is 56 | // the variable of integration 57 | if(context.isConstant(expr.args[0]) && expr.args[1].equals(context.variable)) { 58 | // Yay, we matched 'myUpperGamma(s,x)', so we know the integral! 59 | return new OperatorNode('-', 'subtract', [ 60 | new OperatorNode('*', 'multiply', [ 61 | context.variable, 62 | new FunctionNode('myUpperGamma', [ 63 | expr.args[0], 64 | context.variable 65 | ]) 66 | ]), 67 | new FunctionNode('myUpperGamma', [ 68 | new OperatorNode('+', 'add', [ 69 | expr.args[0], 70 | new ConstantNode(1) 71 | ]), 72 | context.variable 73 | ]) 74 | ]); 75 | } 76 | } 77 | } 78 | // Our rule, didn't apply :( 79 | // return undefined 80 | } 81 | ``` 82 | 83 | Now that we have our custom integration rule, we can add it to the list of standard rules and use this combined list to find integrals involving `myUpperGamma`! 84 | 85 | ```javascript 86 | // Define our integration rules to include our custom rule 87 | var options = { rules: math.integral.rules.concat([myUpperGammaRule]) }; 88 | 89 | // Compute integrals of our function! 90 | math.integral('myUpperGamma(a,x)', 'x', options); 91 | // 'x*myUpperGamma(a,x) - myUpperGamma(a+1,x)' 92 | 93 | math.integral('myUpperGamma(a^2,x) + 13', 'x', options); 94 | // 'x*myUpperGamma(a^2,x) - myUpperGamma(a^2+1,x) + 13*x' 95 | 96 | math.integral('myUpperGamma(a,5*x+99)', 'x', options); 97 | // '((5*x+99) * myUpperGamma(a, 5*x+99) - myUpperGamma(a+1, 5*x+99)) / 5' 98 | 99 | math.integral('sqrt(myUpperGamma(a,x)) * sqrt(myUpperGamma(a,x))', 'x', options); 100 | // 'x*myUpperGamma(a,x) - myUpperGamma(a+1,x)' 101 | ``` 102 | 103 | Now suppose in addition to `myUpperGamma` we have another custom function `mySum`: just like `add`, it will accept a variable number of arguments and add the arguments together, but it also performs some other non-mathematical function (such as logging each argument before evaluation, or checking to ensure that none of its arguments are `NaN`). Now, we can add rules to the integrator that represent the linearity of the integral over `mySum`: 104 | 105 | ```javascript 106 | // integral(mySum(f(x), g(x), ...), x) = mySum(integral(f(x), x), integral(g(x), x), ...) 107 | function mySumRule(expr, context, subIntegral) { 108 | // Ensure we are trying to integrate a FunctionNode 109 | if(expr.type === "FunctionNode") { 110 | // Ensure we are trying to integrate 'mySum' 111 | if(expr.name === "mySum") { 112 | // Try to find integrals of all the terms in the sum 113 | var termIntegrals = expr.args.map(function(term) { 114 | return subIntegral(term, context, 'sum rule (mySum)'); 115 | }); 116 | 117 | // Only if all terms had integrals did we actually find an integral 118 | if(termIntegrals.every(function(termInt) { return !!termInt; })) { 119 | // Yay, we found the integral! 120 | return new FunctionNode('mySum', termIntegrals); 121 | } 122 | } 123 | } 124 | // return undefined 125 | } 126 | ``` 127 | 128 | Note how we use the `subIntegral` callback to find the integral of all the terms in the sum, and then returns an integral for the entire `mySum` expression only if all the individual terms could be integrated. Now, if we use both of our custom rules, we can integrate expressions with both `mySum` and `myUpperGamma`: 129 | 130 | ```javascript 131 | var options = { rules: math.integral.rules.concat([myUpperGammaRule, mySumRule]) }; 132 | 133 | math.integral("mySum(3*x^2, x, 1/x, 1)", "x", options); 134 | // 'mySum(x^3, x^2/2, log(abs(x)), x)' 135 | 136 | math.integral("mySum(2*x, myUpperGamma(a,x))", "x", options); 137 | // 'mySum(x^2, x*myUpperGamma(a,x) - myUpperGamma(a+1,x))' 138 | ``` 139 | 140 | ### Debug Output 141 | The options object can have a property `debugPrint` that, if set to `true`, will instruct the integrator to "show its work": that is, it will print to the console all steps taken and all rules applied in a particular integration. For example, `integral("x^2 + sin(pi*x)", "x", {debugPrint: true})` produces the following output on the console: 142 | 143 | ``` 144 | find integral of (x ^ 2) + (sin(pi * x)) dx 145 | sum rule: find integral of x ^ 2 dx 146 | Computed: (1 / (2 + 1)) * (x ^ (2 + 1)) 147 | sum rule: find integral of sin(pi * x) dx 148 | linear substitution: find integral of sin(x) dx 149 | Computed: -(cos(x)) 150 | Computed: (-(cos(pi * x))) / (pi * 1) 151 | Computed: ((1 / (2 + 1)) * (x ^ (2 + 1))) + ((-(cos(pi * x))) / (pi * 1)) 152 | ``` 153 | 154 | Using `debugPrint` is a good way to learn how the integration rules interact and combine to find integrals, and is extremely helpful when developing custom integration rules. 155 | 156 | 157 | ## Algorithm 158 | The integration algorithm used by this implementation is based on a depth-first search memoized pattern matching. Each pattern is specified as a function that attempts to either compute the integral directly, or to rewrite or split the integrand into an expression that is easier to integrate, and computing the integral of that. The core of the integrator is rule agnostic: by default it uses a set of standard integration rules stored at `integral.rules`, although using custom rules is supported (see the [Custom Rules](#custom-rules) section). 159 | 160 | The memoization of integrands offers not only a speedup for common integrals of sub-expressions, but also prevents infinite loops if two rules undo each other (such as if one multiplies out exponents, and the other combines these common factors). However, this algorithm is still susceptible to a rule (or to several interacting rules) that can apply an infinite number of times producing a new, unique integrand every application; such a rule would cause the integrator to recurse indefinitely (or, until all stack space or memory is consumed). 161 | 162 | 163 | ### Limitations 164 | Due to the simplicity of this approach (and the relative early stage of development of this package), there are many limitations in this current implementation. To give an incomplete list, there are currently no rules to support the following: 165 | - Integration-by-parts or u-substitution (except for u-substitution of a linear function when the variable of integration is unique in the integrand). This means we cannot integrate the following expressions: 166 | - `2 * x * cos(x^2)` 167 | - `3 * x^2 * e^(x^3)` 168 | - `x * ln(x)` 169 | - `x^3 * e^x` 170 | - Partial fractions decomposition to integrate rational functions 171 | - Inverse trigonometric functions 172 | - Trigonometric simplification, as is needed for expressions like `cos(x) * tan(x)` 173 | - Integrating `e ^ -(x^2)` and the like using [`erf`](https://en.wikipedia.org/wiki/Error_function) 174 | 175 | If implement a rule to handle one of these cases (or any other currenly unsupported case), please submit a pull request! 176 | 177 | 178 | ## License 179 | Copyright (c) 2018 Joel Hoover (joelahoover@gmail.com) 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 182 | 183 | http://www.apache.org/licenses/LICENSE-2.0 184 | 185 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 186 | -------------------------------------------------------------------------------- /test/integral.test.js: -------------------------------------------------------------------------------- 1 | var mathRaw = require('mathjs'); 2 | var expect = require('chai').expect; 3 | require('mocha'); 4 | 5 | var integral = require('../lib/integral'); 6 | 7 | describe('Integral Function', function() { 8 | 9 | function getMathWithIntegral() { 10 | var math = mathRaw.create(); 11 | math.import(integral); 12 | return math; 13 | } 14 | 15 | function compareSimplifiedStrings(left, right) { 16 | expect(mathRaw.simplify(left).toString()).to.equal(mathRaw.simplify(right).toString()); 17 | } 18 | 19 | it('should exist in mathjs after being imported', function() { 20 | var math = getMathWithIntegral(); 21 | expect(math).to.respondTo('integral'); 22 | }); 23 | 24 | it('should be callable with any combinations of parameter types', function() { 25 | var math = getMathWithIntegral(); 26 | 27 | var options = { simplify: false }; 28 | var x = math.parse("x"); 29 | expect(math.integral.bind(null, "x", "x", options)).to.not.throw(); 30 | expect(math.integral.bind(null, "x", "x")).to.not.throw(); 31 | expect(math.integral.bind(null, "x", x, options)).to.not.throw(); 32 | expect(math.integral.bind(null, "x", x)).to.not.throw(); 33 | expect(math.integral.bind(null, x, "x", options)).to.not.throw(); 34 | expect(math.integral.bind(null, x, "x")).to.not.throw(); 35 | expect(math.integral.bind(null, x, x, options)).to.not.throw(); 36 | expect(math.integral.bind(null, x, x)).to.not.throw(); 37 | }); 38 | 39 | it('should find basic integrals', function() { 40 | var math = getMathWithIntegral(); 41 | 42 | compareSimplifiedStrings(math.integral("1", "x"), "x"); 43 | compareSimplifiedStrings(math.integral("x", "x"), "1/2*x^2"); 44 | compareSimplifiedStrings(math.integral("+x", "x"), "1/2*x^2"); 45 | compareSimplifiedStrings(math.integral("-x", "x"), "-1/2*x^2"); 46 | compareSimplifiedStrings(math.integral("(((x)))", "x"), "1/2*x^2"); 47 | compareSimplifiedStrings(math.integral("x + 1", "x"), "1/2*x^2 + x"); 48 | compareSimplifiedStrings(math.integral("2*x", "x"), "x^2"); 49 | compareSimplifiedStrings(math.integral("x/2", "x"), "1/4*x^2"); 50 | compareSimplifiedStrings(math.integral("x/x", "x"), "x"); 51 | }); 52 | 53 | it('should find integrals of polynomials', function() { 54 | var math = getMathWithIntegral(); 55 | 56 | compareSimplifiedStrings(math.integral("x^2", "x"), "1/3*x^3"); 57 | compareSimplifiedStrings(math.integral("x^3", "x"), "1/4*x^4"); 58 | compareSimplifiedStrings(math.integral("8*x^3 + 3x^2 + 1", "x"), "2*x^4+x^3+x"); 59 | compareSimplifiedStrings(math.integral("11*x^10", "x"), "x^11"); 60 | }); 61 | 62 | it('should find integrals of polynomial expression not written in standard form', function() { 63 | var math = getMathWithIntegral(); 64 | 65 | compareSimplifiedStrings(math.integral("12 * (2 * x + 1)", "x"), "12*(x^2 + x)"); 66 | compareSimplifiedStrings(math.integral("10 * x * 4 * y", "x"), "20*y*x^2"); 67 | compareSimplifiedStrings(math.integral("12 * (x * x)", "x"), "4*x^3"); 68 | compareSimplifiedStrings(math.integral("12 * x * x", "x"), "4*x^3"); 69 | compareSimplifiedStrings(math.integral("(12 * x) * x", "x"), "4*x^3"); 70 | compareSimplifiedStrings(math.integral("3 * +x * -x", "x"), "-x^3"); 71 | compareSimplifiedStrings(math.integral("x * (4 * x^2 + (1+1)*y)", "x"), "x^4 + y*x^2"); 72 | compareSimplifiedStrings(math.integral("(x + 1) * (x - 1)", "x"), "1/3*x^3 - x"); 73 | compareSimplifiedStrings(math.integral("(-x)^3", "x"), "-1/4*(-x)^4"); 74 | compareSimplifiedStrings(math.integral("(x + 1)^3", "x"), "1/4*(x+1)^4"); 75 | compareSimplifiedStrings(math.integral("((1/8)*x + 1)^3", "x"), "2*((1/8)*x+1)^4"); 76 | compareSimplifiedStrings(math.integral("x * (x+1)^2 * x", "x"), "1/2 * x^4 + 1/5 * x^5 + x^3/3"); 77 | 78 | compareSimplifiedStrings(math.integral("add(2, x, 1)", "x"), "3*x + x^2/2"); 79 | compareSimplifiedStrings(math.integral("subtract(3*x^2, 1)", "x"), "x^3 - x"); 80 | compareSimplifiedStrings(math.integral("multiply(x, 5, x, x^2)", "x"), "x^5"); 81 | compareSimplifiedStrings(math.integral("divide(x, 10)", "x"), "x^2/20"); 82 | }); 83 | 84 | it('should find integrals with x to non-integer, negative, and variable powers', function() { 85 | var math = getMathWithIntegral(); 86 | 87 | compareSimplifiedStrings(math.integral("x^3.5", "x"), "1/4.5*x^4.5"); 88 | compareSimplifiedStrings(math.integral("x^-3", "x"), "-1/2*x^-2"); 89 | compareSimplifiedStrings(math.integral("x^-3.6", "x"), "-1/2.6*x^-2.6"); 90 | compareSimplifiedStrings(math.integral("1/x", "x"), "log(abs(x))"); 91 | compareSimplifiedStrings(math.integral("x^-1", "x"), "log(abs(x))"); 92 | compareSimplifiedStrings(math.integral("x^(6-6.5 + 8-8.5)", "x"), "log(abs(x))"); 93 | compareSimplifiedStrings(math.integral("1/(x^-4)", "x"), "1/5*x^5"); 94 | compareSimplifiedStrings(math.integral("x^(b-1)", "x"), "1/b * x^b"); 95 | compareSimplifiedStrings(math.integral("sqrt(x)", "x"), "2/3*x^(3/2)"); 96 | compareSimplifiedStrings(math.integral("nthRoot(x,6)", "x"), "x^(7/6) * 6/7"); 97 | compareSimplifiedStrings(math.integral("nthRoot(e,1/x)", "x"), "e^x"); 98 | }); 99 | 100 | it('should find integrals of exponential functions', function() { 101 | var math = getMathWithIntegral(); 102 | 103 | compareSimplifiedStrings(math.integral("e^x", "x"), "e^x"); 104 | compareSimplifiedStrings(math.integral("2^x", "x"), "2^x / log(2)"); 105 | compareSimplifiedStrings(math.integral("a^x", "x"), "a^x / log(a)"); 106 | compareSimplifiedStrings(math.integral("e^(x+1)", "x"), "e^(x+1)"); 107 | compareSimplifiedStrings(math.integral("e^(2*x+1)", "x"), "1/2*e^(2*x+1)"); 108 | compareSimplifiedStrings(math.integral("e^-x", "x"), "-e^-x"); 109 | compareSimplifiedStrings(math.integral("(e^x)^b", "x"), "e^(x*b)/b"); 110 | compareSimplifiedStrings(math.integral("pow(a, -x)", "x"), "-(1/log(a) * a^(-x))"); 111 | compareSimplifiedStrings(math.integral("exp(x)", "x"), "e^x"); 112 | }); 113 | 114 | it('should find integrals of logrithmic functions', function() { 115 | var math = getMathWithIntegral(); 116 | 117 | compareSimplifiedStrings(math.integral("log(x)", "x"), "x*log(x) - x"); 118 | compareSimplifiedStrings(math.integral("log(x, 10)", "x"), "1/log(10)*(x*log(x) - x)"); 119 | compareSimplifiedStrings(math.integral("1/log(10, x)", "x"), "1/log(10)*(x*log(x) - x)"); 120 | }); 121 | 122 | it('should find integrals of basic trigonometric functions', function() { 123 | var math = getMathWithIntegral(); 124 | 125 | compareSimplifiedStrings(math.integral("sin(x)", "x"), "-cos(x)"); 126 | compareSimplifiedStrings(math.integral("cos(x)", "x"), "sin(x)"); 127 | compareSimplifiedStrings(math.integral("tan(x)", "x"), "log(abs(sec(x)))"); 128 | compareSimplifiedStrings(math.integral("sec(x)", "x"), "log(abs(sec(x) + tan(x)))"); 129 | compareSimplifiedStrings(math.integral("csc(x)", "x"), "log(abs(csc(x) - cot(x)))"); 130 | compareSimplifiedStrings(math.integral("cot(x)", "x"), "log(abs(sin(x)))"); 131 | 132 | compareSimplifiedStrings(math.integral("cos(2*x+1)", "x"), "sin(2*x+1)/2"); 133 | compareSimplifiedStrings(math.integral("sec(pi*(x+1/2))", "x"), "log(abs(sec(pi*(x+1/2)) + tan(pi*(x+1/2))))/pi"); 134 | }); 135 | 136 | it('should use custom rules while integrating', function() { 137 | var math = getMathWithIntegral(); 138 | var OperatorNode = math.expression.node.OperatorNode; 139 | var FunctionNode = math.expression.node.FunctionNode; 140 | var ConstantNode = math.expression.node.ConstantNode; 141 | 142 | var rules = math.integral.rules.concat([ 143 | // myfunc is actually just the incomplete gamma function 144 | // http://www.wolframalpha.com/input/?i=integral+gamma(a,x)+dx 145 | // integral(myfunc(c, x), x) = x*myfunc(c, x) - myfunc(c+1, x) 146 | function(expr, context, subIntegral) { 147 | // Ensure we are trying to integrate a FunctionNode 148 | if(expr.type === "FunctionNode") { 149 | // Ensure we are trying to integrate 'myfunc' and that it has 2 arguments 150 | if(expr.name === "myfunc" && expr.args.length === 2) { 151 | // Ensure that the first argument is constant and the second one is 152 | // the variable of integration (as we can only find an integral under 153 | // those constraints) 154 | if(context.isConstant(expr.args[0]) && expr.args[1].equals(context.variable)) { 155 | // Yay, we found the integral! 156 | return new OperatorNode('-', 'subtract', [ 157 | new OperatorNode('*', 'multiply', [ 158 | context.variable, 159 | new FunctionNode('myfunc', [ 160 | expr.args[0], 161 | context.variable 162 | ]) 163 | ]), 164 | new FunctionNode('myfunc', [ 165 | new OperatorNode('+', 'add', [ 166 | expr.args[0], 167 | new ConstantNode(1) 168 | ]), 169 | context.variable 170 | ]) 171 | ]); 172 | } 173 | } 174 | } 175 | // Our rule, didn't apply :( 176 | // return undefined 177 | }, 178 | // mysum is just a regular addition, but maybe it does some logging or such, 179 | // so we want to keep it. However, we want to integrate using the linearity 180 | // property of mysum. 181 | // integral(mysum(f(x), g(x), ...), x) = mysum(integral(f(x), x), integral(g(x), x), ...) 182 | function(expr, context, subIntegral) { 183 | // Ensure we are trying to integrate a FunctionNode 184 | if(expr.type === "FunctionNode") { 185 | // Ensure we are trying to integrate 'mysum' 186 | if(expr.name === "mysum") { 187 | // Try to find integrals of all the terms in the sum 188 | var termIntegrals = expr.args.map(function(term) { 189 | return subIntegral(term, context); 190 | }); 191 | 192 | // Only if all terms had integrals did we actually find an integral 193 | if(termIntegrals.every(function(termInt) { return !!termInt; })) { 194 | // Yay, we found the integral! 195 | return new FunctionNode('mysum', termIntegrals); 196 | } 197 | } 198 | } 199 | } 200 | ]); 201 | var options = { rules: rules }; 202 | 203 | compareSimplifiedStrings(math.integral("myfunc(a,x)", "x", options), "x*myfunc(a,x) - myfunc(a+1,x)"); 204 | compareSimplifiedStrings(math.integral("myfunc(a,x) + 13", "x", options), "x*myfunc(a,x) - myfunc(a+1,x) + 13*x"); 205 | compareSimplifiedStrings(math.integral("myfunc(a^2,5*x+99)", "x", options), "((5*x+99)*myfunc(a^2,5*x+99) - myfunc(a^2+1,5*x+99))/5"); 206 | compareSimplifiedStrings(math.integral("sqrt(myfunc(a,x)) * sqrt(myfunc(a,x))", "x", options), "x*myfunc(a,x) - myfunc(a+1,x)"); 207 | 208 | compareSimplifiedStrings(math.integral("mysum(3*x^2, x, 1/x, 1)", "x", options), "mysum(x^3, x^2/2, log(abs(x)), x)"); 209 | compareSimplifiedStrings(math.integral("mysum(2*x, myfunc(a,x))", "x", options), "mysum(x^2, x*myfunc(a,x) - myfunc(a+1,x))"); 210 | }) 211 | 212 | it.skip('should not be too slow', function() { 213 | var math = getMathWithIntegral(); 214 | 215 | // This test is 3-4 times slower with simplify set to true 216 | var options = { simplify: false }; 217 | 218 | for(var i = 0; i < 250; i+=6) { 219 | math.integral("3 * +x * -x", "x", options); 220 | math.integral("(x + 1) * (x - 1)", "x", options); 221 | math.integral("8*x^3 + 3x^2 + 1", "x", options); 222 | math.integral("((1/8)*x + 1)^10", "x", options); 223 | math.integral("e^(2*x+1)", "x", options); 224 | math.integral("cos(2*x+1)", "x", options); 225 | } 226 | }); 227 | 228 | it('should fail to find an integral when no integral exists', function() { 229 | var math = getMathWithIntegral(); 230 | 231 | expect(math.integral.bind(null, "e^(x^2)", "x")).to.throw(); 232 | expect(math.integral.bind(null, "log(log(x))", "x")).to.throw(); 233 | expect(math.integral.bind(null, "1/log(x)", "x")).to.throw(); 234 | expect(math.integral.bind(null, "e^x / x", "x")).to.throw(); 235 | expect(math.integral.bind(null, "e^(e^x)", "x")).to.throw(); 236 | expect(math.integral.bind(null, "sin(x^2)", "x")).to.throw(); 237 | expect(math.integral.bind(null, "sin(x)/x", "x")).to.throw(); 238 | }); 239 | 240 | }); 241 | -------------------------------------------------------------------------------- /lib/integral.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function factory(type, config, load, typed) { 4 | var parse = load(require('mathjs/lib/expression/parse')); 5 | var simplify = load(require('mathjs/lib/function/algebra/simplify')); 6 | var simplifyConstant = load(require('mathjs/lib/function/algebra/simplify/simplifyConstant')); 7 | var ConstantNode = load(require('mathjs/lib/expression/node/ConstantNode')); 8 | var FunctionNode = load(require('mathjs/lib/expression/node/FunctionNode')); 9 | var OperatorNode = load(require('mathjs/lib/expression/node/OperatorNode')); 10 | var SymbolNode = load(require('mathjs/lib/expression/node/SymbolNode')); 11 | 12 | /** 13 | * Finds the integral of an expression with respect to the given variable. 14 | * 15 | * The basic algorithm works as follows: the integrand to each function in the 16 | * `rules` array, and returns the result of the first function that manages 17 | * to find an integral. Each function is also able to recursively call the 18 | * integral function, allowing for multiple rules to be applied successively. 19 | * 20 | * Integration is a rather hard problem, and as such, this implementation can 21 | * only find integrals for relatively simple integrands. If it cannot find the 22 | * integral for the given expression, it will throw an error. 23 | * 24 | * @param {Node | string} expr The expression to be integrated 25 | * @param {SymbolNode | string} variable The variable of integration 26 | * @param {{rules: Array., simplify: boolean, {debugPrint: boolean}}} [options] 27 | * The options for how to integrate: `rules` is the array of rules that 28 | * is applied to the integrand, `simplify` determines wether the output 29 | * is simplified or not, and `debugPrint` determines if the integrator's 30 | * steps are recorded to `console.log` or just ignored 31 | * @return {Node} Return the computed integral 32 | */ 33 | var integral = typed('integral', { 34 | 'Node, SymbolNode, Object': function(expr, variable, options) { 35 | // Apply defaults to options object 36 | if(options.simplify === undefined) options.simplify = true; 37 | if(options.debugPrint === undefined) options.debugPrint = false; 38 | if(options.rules === undefined) options.rules = integral.rules; 39 | 40 | // Create an integration context for this integral 41 | var context = new IntegrationContext(variable, options); 42 | 43 | // Simplify the integral 44 | var simplifiedExpr = preprocessIntegrand(expr, context); 45 | var integralResult = _integral(simplifiedExpr, context); 46 | 47 | if(!integralResult) { 48 | throw new Error('Unable to find integral of "' + expr + '" with respect to "' + variable + '"'); 49 | } 50 | 51 | // Ensure that all nodes are unique 52 | integralResult = integralResult.cloneDeep(); 53 | 54 | // Return the simplified expression if specified in options, or the unsimplified 55 | // integral otherwise 56 | return options.simplify === true ? simplify(integralResult) : integralResult; 57 | }, 58 | 59 | 'Node, SymbolNode': function(expr, variable) { 60 | return integral(expr, variable, {}); 61 | }, 62 | 63 | 'string, SymbolNode, Object': function(expr, variable, options) { 64 | return integral(parse(expr), variable, options); 65 | }, 66 | 67 | 'string, SymbolNode': function(expr, variable) { 68 | return integral(parse(expr), variable); 69 | }, 70 | 71 | 'Node, string, Object': function(expr, variable, options) { 72 | return integral(expr, parse(variable), options); 73 | }, 74 | 75 | 'Node, string': function(expr, variable) { 76 | return integral(expr, parse(variable)); 77 | }, 78 | 79 | 'string, string, Object': function(expr, variable, options) { 80 | return integral(parse(expr), parse(variable), options); 81 | }, 82 | 83 | 'string, string': function(expr, variable) { 84 | return integral(parse(expr), parse(variable)); 85 | } 86 | 87 | }); 88 | 89 | /** 90 | * @constructor IntegrationContext 91 | * 92 | * Encapsulates an integration context, including the following: the variable 93 | * of integration; a memoizing isConstant function (to determine if a given 94 | * expression is constant relative to the variable of integration); and a lookup 95 | * table for already computed subintegrals. 96 | * 97 | * @param {SymbolNode} variable The variable of integration. 98 | * @param {Object} options The integration options object. 99 | */ 100 | function IntegrationContext(variable, options) { 101 | this.variable = variable.clone(); 102 | this.options = options; 103 | 104 | this._constantExpr = {}; 105 | this.subIntegral = {}; 106 | 107 | this.rules = options.rules; 108 | 109 | this.debugIndent = 0; 110 | } 111 | 112 | /** 113 | * Determines if the given expression is constant in this context. An expression 114 | * is constant if and only if it does not contain the integration variable for 115 | * this context. 116 | * 117 | * @param {Node} expr The expression to test for constantness. 118 | * @return {boolean} True if the expression is constant, false otherwise. 119 | */ 120 | IntegrationContext.prototype.isConstant = function(expr) { 121 | if(typeof this._constantExpr[expr] === 'boolean') { 122 | return this._constantExpr[expr]; 123 | } else { 124 | // We must determine if this expression is constant ourselves 125 | return (this._constantExpr[expr] = isConstantHelper(expr, this)); 126 | } 127 | 128 | function isConstantHelper(expr, self) { 129 | switch(expr.type) { 130 | case "ConstantNode": 131 | return true; 132 | case "SymbolNode": 133 | return expr.name !== self.variable.name; 134 | case "OperatorNode": 135 | return expr.args.every(self.isConstant.bind(self)); 136 | case "ParenthesisNode": 137 | return self.isConstant(expr.content); 138 | case "FunctionNode": 139 | return expr.args.every(self.isConstant.bind(self)); 140 | default: 141 | throw new Error("Node type '" + expr.type + "' is currently unsupported in isConstant."); 142 | } 143 | } 144 | }; 145 | 146 | /** 147 | * Print some debug text about cacluating the integral in this context. 148 | */ 149 | IntegrationContext.prototype.printDebug = function(text) { 150 | if(this.options.debugPrint) { 151 | var indent = ""; 152 | for(var i = 0; i < this.debugIndent; i++) { 153 | indent += " "; 154 | } 155 | console.log(indent + text); 156 | } 157 | } 158 | 159 | /** 160 | * Prepare the given expression for integration by 161 | * - Remove parentheses 162 | * - Reduce standard functions into more generic structures: 163 | * - Convert 'add', 'subtract', 'multiply', and 'divide' into their operator counterparts 164 | * - Convert 'sqrt', 'nthRoot', 'exp', and 'pow' into '^' operator 165 | * - Convert 2-arg 'log' into quotient of natrual logarithms 166 | * - Convert division into multiplication to power of -1 (only in non-constant nodes) 167 | * 168 | * @param {Node} expr The expression tree representing the integrand to be processed 169 | * @param {Object} context The integration context to interpret the integrand 170 | * @return {Node} The processed integrand 171 | */ 172 | function preprocessIntegrand(expr, context) { 173 | expr = removeParenthesis(expr); 174 | expr = reduceFunctions(expr); 175 | expr = removeDivision(expr); 176 | return expr; 177 | 178 | function removeParenthesis(node) { 179 | if(node.type === "ParenthesisNode") { 180 | return removeParenthesis(node.content); 181 | } else { 182 | return node.map(removeParenthesis); 183 | } 184 | } 185 | 186 | function reduceFunctions(expr) { 187 | return helper(expr); 188 | 189 | function helper(expr) { 190 | if(!context.isConstant(expr) && expr.type === "FunctionNode") { 191 | var funcName = typeof expr.fn === "string" ? expr.fn : expr.fn.name; 192 | 193 | switch(funcName) { 194 | case "add": 195 | return new OperatorNode('+', 'add', expr.args); 196 | case "subtract": 197 | return new OperatorNode('-', 'subtract', expr.args); 198 | case "multiply": 199 | return new OperatorNode('*', 'multiply', expr.args); 200 | case "divide": 201 | return new OperatorNode('/', 'divide', expr.args); 202 | case "sqrt": 203 | return new OperatorNode('^', 'pow', [ 204 | expr.args[0].map(helper), 205 | new OperatorNode('/', 'divide', [ 206 | new ConstantNode(1), 207 | new ConstantNode(2) 208 | ]) 209 | ]); 210 | case "nthRoot": 211 | return new OperatorNode('^', 'pow', [ 212 | expr.args[0].map(helper), 213 | new OperatorNode('/', 'divide', [ 214 | new ConstantNode(1), 215 | expr.args[1].map(helper) 216 | ]) 217 | ]); 218 | case "exp": 219 | return new OperatorNode('^', 'pow', [new SymbolNode('e'), expr.args[0]]); 220 | case "pow": 221 | return new OperatorNode('^', 'pow', expr.args); 222 | case "log": 223 | if(expr.args.length === 2) { 224 | return new OperatorNode('/', 'divide', [ 225 | new FunctionNode('log', [expr.args[0].map(helper)]), 226 | new FunctionNode('log', [expr.args[1].map(helper)]) 227 | ]); 228 | } else { 229 | break; 230 | } 231 | default: 232 | break; 233 | } 234 | } 235 | return expr.map(helper); 236 | } 237 | } 238 | 239 | function removeDivision(expr) { 240 | return expr.transform(function(node) { 241 | if(!context.isConstant(node) && node.type === 'OperatorNode' && node.op === '/') { 242 | return new OperatorNode('*', 'multiply', [ 243 | node.args[0], 244 | new OperatorNode('^', 'pow', [ 245 | node.args[1], 246 | new ConstantNode(-1) 247 | ]) 248 | ]); 249 | } else { 250 | return node; 251 | } 252 | }); 253 | } 254 | } 255 | 256 | /** 257 | * Gets the numerical value of the given node if possible, othewise returns undefined. 258 | * TODO: does this work with BigNumber / Fractions / etc. 259 | */ 260 | function getNumericValue(expr) { 261 | var simplified = simplifyConstant(expr); 262 | return toNumber(simplified); 263 | 264 | function toNumber(expr) { 265 | if(expr.type === 'OperatorNode' && expr.op === '-' && expr.args.length === 1) { 266 | // Unary minus 267 | var num = toNumber(expr.args[0]); 268 | return num === undefined ? undefined : -num; 269 | } else if(expr.type === 'ConstantNode' && (!expr.valueType || expr.valueType === 'number')) { 270 | return +expr.value; 271 | } else { 272 | return undefined; 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * @name IntegrationRule 279 | * @function 280 | * @param {Node} expr The expression that is being integrated. 281 | * @param {IntegrationContext} context The integration context. 282 | * @param {function(expr: Node, context: IntegrationContext, ?ruleName: string)} subIntegral 283 | * Callback that attempts to integrate the given expression recursively. 284 | * @return {Node | undefined | null} Returns the integrated expression, or undefined 285 | * or null if unable to find integral. 286 | */ 287 | 288 | /** 289 | * The default rules for integration. 290 | * 291 | * @type {Array.} 292 | */ 293 | integral.rules = [ 294 | // Simplify constants in the integral 295 | function(expr, context, subIntegral) { 296 | var simplified = simplify.simplifyCore(expr, context); 297 | if(!simplified.equals(expr)) { 298 | return subIntegral(simplified, context, "simplified expression"); 299 | } 300 | }, 301 | 302 | // Ignore parentheses 303 | function(expr, context, subIntegral) { 304 | if(expr.type === 'ParenthesisNode') { 305 | var int = subIntegral(expr.content, context, "parentheses removal"); 306 | return int ? new ParenthesisNode(int) : null; 307 | } 308 | }, 309 | 310 | // integral(c, x) = c * x 311 | function(expr, context) { 312 | if(context.isConstant(expr)) { 313 | return new OperatorNode('*', 'multiply', [ 314 | expr.clone(), 315 | context.variable.clone() 316 | ]); 317 | } 318 | }, 319 | 320 | // integral(+/- f(x), x) = +/- integral(f(x), x) and 321 | // integral(f(x) +/- g(x), x) = integral(f(x), x) +/- integral(g(x), x) 322 | function(expr, context, subIntegral) { 323 | if(expr.type === "OperatorNode" && (expr.op === '+' || expr.op === '-')) { 324 | var childInts = expr.args.map(function(expr) { 325 | return subIntegral(expr, context, "sum rule"); 326 | }); 327 | 328 | if(childInts.every(function(n) { return n; })) { 329 | return new OperatorNode(expr.op, expr.fn, childInts); 330 | } 331 | } 332 | }, 333 | 334 | // integral(f(x) * (g(x) * h(x)), x) = integral(f(x) * g(x) * h(x), x) 335 | function(expr, context, subIntegral) { 336 | if(expr.type === "OperatorNode" && expr.op === "*") { 337 | var factors = getFactors(expr); 338 | if(factors.length > expr.args.length) { 339 | return subIntegral(new OperatorNode('*', 'multiply', factors), context, "product flattening"); 340 | } 341 | } 342 | 343 | function getFactors(expr) { 344 | if(expr.type === "OperatorNode" && expr.op === "*") { 345 | return expr.args.reduce(function(factors, expr) { 346 | return factors.concat(getFactors(expr)); 347 | }, []); 348 | } else if (expr.type === "ParenthesisNode") { 349 | return getFactors(expr.content) 350 | } else { 351 | return [expr]; 352 | } 353 | } 354 | }, 355 | 356 | // integral(x, x) = 1/2*x^2 357 | function(expr, context) { 358 | if(expr.type === "SymbolNode" && expr.name === context.variable.name) { 359 | return new OperatorNode('*', 'multiply', [ 360 | new OperatorNode('/', 'divide', [ 361 | new ConstantNode(1), 362 | new ConstantNode(2) 363 | ]), 364 | new OperatorNode('^', 'pow', [ 365 | expr.clone(), 366 | new ConstantNode(2) 367 | ]) 368 | ]); 369 | } 370 | }, 371 | 372 | // integral(c*f(x), x) = c*integral(f(x), x) 373 | function(expr, context, subIntegral) { 374 | if(expr.type === "OperatorNode" && expr.op === '*') { 375 | var constantFactors = []; 376 | var nonConstantFactors = []; 377 | expr.args.forEach(function(expr) { 378 | if(context.isConstant(expr)) { 379 | constantFactors.push(expr); 380 | } else { 381 | nonConstantFactors.push(expr); 382 | } 383 | }); 384 | 385 | if(constantFactors.length !== 0) { 386 | var constantNode = factorsToNode(constantFactors); 387 | var nonConstantNode = factorsToNode(nonConstantFactors); 388 | 389 | var nonConstantIntegral = subIntegral(nonConstantNode, context, "multiplication by constant"); 390 | if(nonConstantIntegral) { 391 | return new OperatorNode('*', 'multiply', [constantNode, nonConstantIntegral]); 392 | } 393 | } 394 | 395 | function factorsToNode(factors) { 396 | if(factors.length === 1) { 397 | return factors[0]; 398 | } else { 399 | return new OperatorNode('*', 'multiply', factors); 400 | } 401 | } 402 | } 403 | }, 404 | 405 | // integral(x^c, x) = 1/(c+1) * x^(c+1) and integral(x^(-1)) = log(x) 406 | function(expr, context) { 407 | if(expr.type === "OperatorNode" && expr.op === '^' && expr.args[0].equals(context.variable) && context.isConstant(expr.args[1])) { 408 | // Simplify Exponent if constant 409 | var exponentValue = getNumericValue(expr.args[1]); 410 | if(exponentValue === -1) { 411 | return new FunctionNode('log', [ 412 | new FunctionNode('abs', [ 413 | context.variable.clone() 414 | ]) 415 | ]); 416 | } else { 417 | return new OperatorNode('*', 'multiply', [ 418 | new OperatorNode('/', 'divide', [ 419 | new ConstantNode(1), 420 | new OperatorNode('+', 'add', [ 421 | expr.args[1].clone(), 422 | new ConstantNode(1) 423 | ]) 424 | ]), 425 | new OperatorNode('^', 'pow', [ 426 | expr.args[0].clone(), 427 | new OperatorNode('+', 'add', [ 428 | expr.args[1].clone(), 429 | new ConstantNode(1) 430 | ]) 431 | ]) 432 | ]); 433 | } 434 | } 435 | }, 436 | 437 | // integral(c^x, x) = c^x / log(c) 438 | function(expr, context) { 439 | if(expr.type === 'OperatorNode' && expr.op === '^') { 440 | if(context.isConstant(expr.args[0]) && expr.args[1].equals(context.variable)) { 441 | return new OperatorNode('/', 'divide', [ 442 | expr, 443 | new FunctionNode('log', [expr.args[0]]) 444 | ]); 445 | } 446 | } 447 | }, 448 | 449 | // integral(f(x)^g(x) * f(x)^h(x), x) = integral(f(x)^(g(x)+h(x)), x) 450 | function(expr, context, subIntegral) { 451 | if(expr.type === "OperatorNode" && expr.op === '*') { 452 | var argsAsPower = expr.args.map(getExprInPowerForm); 453 | 454 | // Collect common bases (this is O(n^2) worst case) 455 | var reducedArgs = argsAsPower.reduce(function(acc, exprPower) { 456 | for(var i = 0; i < acc.length; i++) { 457 | if(acc[i].base.equals(exprPower.base)) { 458 | acc[i].power = new OperatorNode('+', 'add', [ 459 | acc[i].power, 460 | exprPower.power 461 | ]); 462 | return acc; 463 | } 464 | } 465 | 466 | // Did not share a common base with any other factor so far 467 | acc.push(exprPower); 468 | return acc; 469 | }, []); 470 | 471 | if(reducedArgs.length < expr.args.length) { 472 | // We combined some things 473 | var reducedExpr = powerFactorsToNode(reducedArgs); 474 | 475 | return subIntegral(reducedExpr, context, "combining powers"); 476 | } 477 | } 478 | 479 | function getExprInPowerForm(expr) { 480 | if(expr.type === "OperatorNode" && expr.op === '^') { 481 | return { 482 | base: expr.args[0], 483 | power: expr.args[1] 484 | }; 485 | } else { 486 | return { 487 | base: expr, 488 | power: new ConstantNode(1) 489 | }; 490 | } 491 | } 492 | 493 | function powerFactorsToNode(factors) { 494 | if(factors.length === 1) { 495 | return powerToNode(factors[0]); 496 | } else { 497 | return new OperatorNode('*', 'multiply', factors.map(powerToNode)); 498 | } 499 | 500 | function powerToNode(powerExpr) { 501 | return new OperatorNode('^', 'pow', [powerExpr.base, powerExpr.power]); 502 | } 503 | } 504 | }, 505 | 506 | // integral((f(x) * g(x))^h(x), x) = integral(f(x)^h(x) * g(x)^h(x)) 507 | function(expr, context, subIntegral) { 508 | if(expr.type === 'OperatorNode' && expr.op === '^') { 509 | var base = expr.args[0]; 510 | var exponent = expr.args[1]; 511 | if(base.type === 'OperatorNode' && base.op === '*') { 512 | return subIntegral(new OperatorNode('*', 'multiply', base.args.map(function(baseChild) { 513 | return new OperatorNode('^', 'pow', [baseChild, exponent]); 514 | })), context, "distributing power"); 515 | } 516 | } 517 | }, 518 | 519 | // integral((f(x) ^ g(x)) ^ h(x), x) = integral(f(x) ^ (g(x) * h(x)), x) 520 | function(expr, context, subIntegral) { 521 | if(expr.type === 'OperatorNode' && expr.op === '^') { 522 | if(expr.args[0].type === 'OperatorNode' && expr.args[0].op === '^') { 523 | return subIntegral(new OperatorNode('^', 'pow', [ 524 | expr.args[0].args[0], 525 | new OperatorNode('*', 'multiply', [ 526 | expr.args[0].args[1], 527 | expr.args[1] 528 | ]) 529 | ]), context, 'removing double exponential'); 530 | } 531 | } 532 | }, 533 | 534 | // integral(f(x) * +/-g(x), x) = +/-integral(f(x) * g(x), x) 535 | function(expr, context, subIntegral) { 536 | if(expr.type === "OperatorNode" && expr.op === '*') { 537 | var wasChange = false; 538 | var isTotalPositive = true; 539 | var processedArgs = []; 540 | expr.args.forEach(function(expr) { 541 | if(expr.type === "OperatorNode" && expr.args.length === 1 && (expr.op === '+' || expr.op === '-')) { 542 | wasChange = true; 543 | isTotalPositive = isTotalPositive ^ (expr.op === '-'); 544 | processedArgs.push(expr.args[0]); 545 | } else { 546 | processedArgs.push(expr); 547 | } 548 | }); 549 | 550 | if(wasChange) { 551 | var int = subIntegral(new OperatorNode('*', 'multiply', processedArgs), context, "removing unary +/- from factors"); 552 | if(int) { 553 | return isTotalPositive ? int : new OperatorNode('-', 'unaryMinus', [int]); 554 | } 555 | } 556 | } 557 | }, 558 | 559 | // integral(f(x) * (g(x) + h(x)), x) = integral(f(x) * g(x) + f(x) * h(x), x) 560 | function(expr, context, subIntegral) { 561 | if(expr.type === "OperatorNode" && expr.op === '*') { 562 | var sumNode = null; 563 | var otherFactors = null; 564 | for(var i = 0; i < expr.args.length; i++) { 565 | if(expr.args[i].type === "OperatorNode" && (expr.args[i].op === '+' || expr.args[i].op === '-')) { 566 | sumNode = expr.args[i]; 567 | otherFactors = expr.args.filter(function(expr, index) { return index !== i; }); 568 | break; 569 | } 570 | } 571 | 572 | if(sumNode !== null) { 573 | var newTerms = sumNode.args.map(function(term) { 574 | return new OperatorNode('*', 'multiply', otherFactors.concat([term])); 575 | }); 576 | return subIntegral(new OperatorNode(sumNode.op, sumNode.fn, newTerms), context, "product distribution"); 577 | } 578 | } 579 | }, 580 | 581 | // integral(f(a*x + b), x) = 1/a * F(a*x + b) where F(u) = integral(f(u), u) 582 | // We also only handle the case where u shows up only once in f(u) 583 | function(expr, context, subIntegral) { 584 | var createIntegralWrapper = null; 585 | 586 | var uniqueParent = getParentOfUniqueVariable(expr); 587 | if(uniqueParent !== null && uniqueParent.type === "OperatorNode") { 588 | if(uniqueParent.op === '+' || uniqueParent.op === '-') { 589 | if(uniqueParent.args.length === 1) { 590 | // unary + or - 591 | createIntegralWrapper = function(int) { 592 | return new OperatorNode(uniqueParent.op, uniqueParent.fn, [int]); 593 | } 594 | } else { 595 | createIntegralWrapper = function(int) { 596 | return int; 597 | } 598 | } 599 | } else if(uniqueParent.op === '*') { 600 | createIntegralWrapper = function(int) { 601 | return new OperatorNode('/', 'divide', [int, 602 | // "remove" the variable of integration 603 | replaceNodeInTree(uniqueParent, context.variable, new ConstantNode(1)) 604 | ]); 605 | }; 606 | } 607 | 608 | if(createIntegralWrapper !== null) { 609 | var preIntegral = replaceNodeInTree(expr, uniqueParent, context.variable.clone()); 610 | var int = subIntegral(preIntegral, context, "linear substitution"); 611 | if(int) { 612 | var backSubstituted = replaceNodeInTree(int, context.variable, uniqueParent); 613 | return createIntegralWrapper(backSubstituted); 614 | } 615 | } 616 | } 617 | 618 | function replaceNodeInTree(expr, node, replacement) { 619 | return replaceHelper(expr); 620 | 621 | function replaceHelper(curNode) { 622 | return node.equals(curNode) ? replacement : curNode.map(replaceHelper); 623 | } 624 | } 625 | 626 | function getParentOfUniqueVariable(expr) { 627 | return helper(expr, null); 628 | 629 | function helper(expr, parent) { 630 | if(context.isConstant(expr)) { 631 | return null; 632 | } else if(expr.type === "SymbolNode" && expr.name === context.variable.name) { 633 | return parent; 634 | } else { 635 | var nonConstantChildren = []; 636 | expr.forEach(function(child) { 637 | if(!context.isConstant(child)) { 638 | nonConstantChildren.push(child); 639 | } 640 | }); 641 | 642 | if(nonConstantChildren.length === 1) { 643 | return helper(nonConstantChildren[0], expr); 644 | } else { 645 | return null; 646 | } 647 | } 648 | } 649 | } 650 | }, 651 | 652 | // integral(f(x)^c [* g(x)], x) = integral(f(x) * f(x)^(c-1) [* g(x)], x) 653 | // However, we only expand for c<=10 to save computational resources 654 | function(expr, context, subIntegral) { 655 | var MaxExponentExpanded = 10; 656 | 657 | if(expr.type === 'OperatorNode' && expr.op === '^') { 658 | var multipliedOut = tryMultiplyOut(expr); 659 | if(multipliedOut) { 660 | var int = subIntegral(multipliedOut, context, "reducing power"); 661 | if(int) { 662 | return int; 663 | } 664 | } 665 | } else if(expr.type === 'OperatorNode' && expr.op === '*') { 666 | for(var i = 0; i < expr.args.length; i++) { 667 | var multipliedOutChild = tryMultiplyOut(expr.args[i]); 668 | if(multipliedOutChild) { 669 | var int = subIntegral(new OperatorNode('*', 'multiply', multipliedOutChild.args.concat( 670 | expr.args.slice(0, i), 671 | expr.args.slice(i+1) 672 | )), context, "reducing power"); 673 | 674 | if(int) { 675 | return int; 676 | } 677 | } 678 | } 679 | } 680 | 681 | // 682 | function tryMultiplyOut(expr) { 683 | if(expr.type === 'OperatorNode' && expr.op === '^' && !context.isConstant(expr.args[0])) { 684 | var exponentValue = getNumericValue(expr.args[1]); 685 | if(Number.isInteger(exponentValue) && exponentValue > 1 && exponentValue <= MaxExponentExpanded) { 686 | return new OperatorNode('*', 'multiply', [ 687 | expr.args[0], 688 | exponentValue === 2 ? expr.args[0] : new OperatorNode('^', 'pow', [ 689 | expr.args[0], 690 | new ConstantNode(exponentValue-1) 691 | ]) 692 | ]); 693 | } 694 | } 695 | 696 | return null; 697 | } 698 | }, 699 | 700 | // integral(log(x), x) = x*log(x) - x 701 | function(expr, context, subIntegral) { 702 | if(expr.type === 'FunctionNode' && expr.name === 'log' && expr.args.length === 1) { 703 | if(expr.args.length === 1 && expr.args[0].equals(context.variable)) { 704 | return new OperatorNode('-', 'subtract', [ 705 | new OperatorNode('*', 'multiply', [ 706 | context.variable, 707 | new FunctionNode('log', [context.variable]) 708 | ]), 709 | context.variable 710 | ]); 711 | } 712 | } 713 | }, 714 | 715 | // integral(sin(x), x) = -cos(x) 716 | // integral(cos(x), x) = sin(x) 717 | // integral(tan(x), x) = log(abs(sec(x))) 718 | // integral(sec(x), x) = log(abs(sec(x) + tan(x))) 719 | // integral(csc(x), x) = log(abs(csc(x) - cot(x))) 720 | // integral(cot(x), x) = log(abs(sin(x))) 721 | function(expr, context, subIntegral) { 722 | if(expr.type === 'FunctionNode' && expr.args[0].equals(context.variable)) { 723 | switch(expr.name) { 724 | case "sin": 725 | return new OperatorNode('-', 'unaryMinus', [ 726 | new FunctionNode("cos", [context.variable]) 727 | ]); 728 | case "cos": 729 | return new FunctionNode("sin", [context.variable]); 730 | case "tan": 731 | //log(abs(sec(x))) 732 | return new FunctionNode('log', [ 733 | new FunctionNode('abs', [ 734 | new FunctionNode('sec', [context.variable]) 735 | ]) 736 | ]); 737 | case "sec": 738 | //log(abs(sec(x) + tan(x))) 739 | return new FunctionNode('log', [ 740 | new FunctionNode('abs', [ 741 | new OperatorNode('+', 'add', [ 742 | new FunctionNode('sec', [context.variable]), 743 | new FunctionNode('tan', [context.variable]) 744 | ]) 745 | ]) 746 | ]); 747 | case "csc": 748 | //log(abs(sec(x) + tan(x))) 749 | return new FunctionNode('log', [ 750 | new FunctionNode('abs', [ 751 | new OperatorNode('-', 'subtract', [ 752 | new FunctionNode('csc', [context.variable]), 753 | new FunctionNode('cot', [context.variable]) 754 | ]) 755 | ]) 756 | ]); 757 | case "cot": 758 | //log(abs(sec(x) + tan(x))) 759 | return new FunctionNode('log', [ 760 | new FunctionNode('abs', [ 761 | new FunctionNode('sin', [context.variable]) 762 | ]) 763 | ]); 764 | default: 765 | return null; 766 | } 767 | } 768 | } 769 | ]; 770 | 771 | /** 772 | * Helper function that runs the main loop for the integrator. It scans over the 773 | * rules until one of them produces an integral or until no more rules are left. 774 | * 775 | * @param {Node} expr The expression to be integrated. 776 | * @param {IntegrationContext} 777 | */ 778 | function _integral(expr, context, lastRuleComment) { 779 | var exprString = expr.toString({ 780 | parenthesis: 'all', 781 | handler: function(node, options) { 782 | if(node.type === 'ParenthesisNode') { 783 | return '(' + node.content.toString(options) + ')'; 784 | } 785 | } 786 | }); 787 | 788 | var debugComment = lastRuleComment ? lastRuleComment + ": " : ""; 789 | debugComment += "find integral of " + exprString + " d" + context.variable.name; 790 | context.printDebug(debugComment); 791 | context.debugIndent++; 792 | 793 | // Check if we already tried to integrate this expression 794 | if(context.subIntegral[exprString] !== undefined) { 795 | // This could be null, indicating that we couldn't find an integral for 796 | // it (or we are currenly working on it a few levels of recursion up!) 797 | context.printDebug("Precomputed: " + context.subIntegral[exprString]); 798 | context.debugIndent--; 799 | return context.subIntegral[exprString]; 800 | } 801 | 802 | // Remember that we are working on this integral, just haven't found a 803 | // solution yet! 804 | context.subIntegral[exprString] = null; 805 | 806 | for(var i = 0; i < context.rules.length; i++) { 807 | var result = context.rules[i](expr, context, _integral); 808 | if(result !== undefined && result !== null) { 809 | // Remember this solution! 810 | context.subIntegral[exprString] = result; 811 | 812 | context.printDebug("Computed: " + result.toString({parenthesis: 'all'})); 813 | context.debugIndent--; 814 | return result; 815 | } 816 | } 817 | 818 | // We couldn't find a solution :( 819 | context.printDebug("No integral found"); 820 | context.debugIndent--; 821 | return null; 822 | } 823 | 824 | return integral; 825 | }; 826 | 827 | exports.name = 'integral'; 828 | exports.factory = factory; 829 | --------------------------------------------------------------------------------