├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── README.mdtex ├── benchmark ├── index.js └── package.json ├── examples ├── comparison.js ├── mathjs.js ├── oscillatory.js └── vector.js ├── images ├── 1-z-32ebeece91.png ├── a-2217a6870d.png ├── b-224c764dec.png ├── int_0011-frac1xcosleftfrac1xrightdx-c5d6a6f216.png ├── int_ab-fx-dx-49d001614b.png ├── oint-fracdzz-2pi-i-3243136d9d.png └── z_0-0-227c53dd15.png ├── index.js ├── package.json ├── test ├── index.js ├── util │ ├── array-equal.js │ └── contour-integrand.js └── vector.js └── vector.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | build 24 | 25 | # Dependency directory 26 | # Deployed apps should consider commenting this line out: 27 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 28 | node_modules 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.mdtex 2 | examples 3 | docs 4 | images 5 | benchmark 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "5.1" 5 | - "4" 6 | - "4.2" 7 | - "4.1" 8 | - "4.0" 9 | - "0.12" 10 | - "0.11" 11 | - "0.10" 12 | - "iojs" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ricky Reusser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # integrate-adaptive-simpson 2 | 3 | [![Build Status](https://travis-ci.org/scijs/integrate-adaptive-simpson.svg)](https://travis-ci.org/scijs/integrate-adaptive-simpson) [![npm version](https://badge.fury.io/js/integrate-adaptive-simpson.svg)](http://badge.fury.io/js/integrate-adaptive-simpson) [![Dependency Status](https://david-dm.org/scijs/integrate-adaptive-simpson.svg)](https://david-dm.org/scijs/integrate-adaptive-simpson) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 4 | 5 | > Compute a definite integral of one variable using [Simpson's Rule](https://en.wikipedia.org/wiki/Simpson%27s_rule) with adaptive quadrature 6 | 7 | ## Introduction 8 | 9 | This module computes the definite integral

\int_a^b f(x) \, dx

using [Romberg Integration](https://en.wikipedia.org/wiki/Romberg%27s_method) based on [Simpson's Rule](https://en.wikipedia.org/wiki/Simpson%27s_rule). That is, it uses [Richardson Extrapolation](https://en.wikipedia.org/wiki/Richardson_extrapolation) to estimate the error and recursively subdivide intervals until the error tolerance is met. The code is adapted from the pseudocode in [Romberg Integration and Adaptive Quadrature](http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf). 10 | 11 | ## Install 12 | 13 | ```bash 14 | $ npm install integrate-adaptive-simpson 15 | ``` 16 | 17 | ## Example 18 | 19 | To compute the definite integral

\int_{0.01}^{1} \frac{1}{x}\cos\left(\frac{1}{x}\right)\,dx,

execute: 20 | 21 | ```javascript 22 | var integrate = require('integrate-adaptive-simpson'); 23 | 24 | function f (x) { 25 | return Math.cos(1 / x) / x; 26 | } 27 | 28 | integrate(f, 0.01, 1, 1e-8); 29 | // => -0.3425527480294604 30 | ``` 31 | 32 | To integrate a vector function, you may import the vectorized version. To compute a contour integral of, say, 1 / z about z_0 = 0, that is,

\oint \frac{dz}{z} = 2\pi i,

33 | 34 | ```javascript 35 | var integrate = require('integrate-adaptive-simpson/vector'); 36 | 37 | integrate(function (f, theta) { 38 | // z = unit circle: 39 | var c = Math.cos(theta); 40 | var s = Math.sin(theta); 41 | 42 | // dz: 43 | var dzr = -s; 44 | var dzi = c; 45 | 46 | // 1 / z at this point on the unit circle: 47 | var fr = c / (c * c + s * s); 48 | var fi = -s / (c * c + s * s); 49 | 50 | // Multiply f(z) * dz: 51 | f[0] = fr * dzr - fi * dzi; 52 | f[1] = fr * dzi + fi * dzr; 53 | }, 0, Math.PI * 2); 54 | 55 | // => [ 0, 6.283185307179586 ] 56 | ``` 57 | 58 | ## API 59 | 60 | #### `require('integrate-adaptive-simpson')( f, a, b [, tol, maxdepth]] )` 61 | 62 | Compute the definite integral of scalar function f from a to b. 63 | 64 | **Arguments:** 65 | - `f`: The function to be integrated. A function of one variable that returns a value. 66 | - `a`: The lower limit of integration, a. 67 | - `b`: The upper limit of integration, b. 68 | - `tol`: The relative error required for an interval to be subdivided, based on Richardson extraplation. Default tolerance is `1e-8`. Be careful—the total accumulated error may be significantly less and result in more function evaluations than necessary. 69 | - `maxdepth`: The maximum recursion depth. Default depth is `20`. If reached, computation continues and a warning is output to the console. 70 | 71 | **Returns**: The computed value of the definite integral. 72 | 73 | #### `require('integrate-adaptive-simpson/vector')( f, a, b [, tol, maxdepth]] )` 74 | 75 | Compute the definite integral of vector function f from a to b. 76 | 77 | **Arguments:** 78 | - `f`: The function to be integrated. The first argument is an array of length `n` into which the output must be written. The second argument is the scalar value of the independent variable. 79 | - `a`: The lower limit of integration, a. 80 | - `b`: The upper limit of integration, b. 81 | - `tol`: The relative error required for an interval to be subdivided, based on Richardson extraplation. Default tolerance is `1e-8`. 82 | - `maxdepth`: The maximum recursion depth. Default depth is `20`. If reached, computation continues and a warning is output to the console. 83 | 84 | **Returns**: An `Array` representing The computed value of the definite integral. 85 | 86 | ## References 87 | Colins, C., [Romberg Integration and Adaptive Quadrature Course Notes](http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf). 88 | 89 | ## License 90 | 91 | (c) 2015 Scijs Authors. MIT License. 92 | -------------------------------------------------------------------------------- /README.mdtex: -------------------------------------------------------------------------------- 1 | # integrate-adaptive-simpson 2 | 3 | [![Build Status](https://travis-ci.org/scijs/integrate-adaptive-simpson.svg)](https://travis-ci.org/scijs/integrate-adaptive-simpson) [![npm version](https://badge.fury.io/js/integrate-adaptive-simpson.svg)](http://badge.fury.io/js/integrate-adaptive-simpson) [![Dependency Status](https://david-dm.org/scijs/integrate-adaptive-simpson.svg)](https://david-dm.org/scijs/integrate-adaptive-simpson) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 4 | 5 | > Compute a definite integral of one variable using [Simpson's Rule](https://en.wikipedia.org/wiki/Simpson%27s_rule) with adaptive quadrature 6 | 7 | ## Introduction 8 | 9 | This module computes the definite integral $$\int_a^b f(x) \, dx$$ using [Romberg Integration](https://en.wikipedia.org/wiki/Romberg%27s_method) based on [Simpson's Rule](https://en.wikipedia.org/wiki/Simpson%27s_rule). That is, it uses [Richardson Extrapolation](https://en.wikipedia.org/wiki/Richardson_extrapolation) to estimate the error and recursively subdivide intervals until the error tolerance is met. The code is adapted from the pseudocode in [Romberg Integration and Adaptive Quadrature](http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf). 10 | 11 | ## Install 12 | 13 | ```bash 14 | $ npm install integrate-adaptive-simpson 15 | ``` 16 | 17 | ## Example 18 | 19 | To compute the definite integral $$\int_{0.01}^{1} \frac{1}{x}\cos\left(\frac{1}{x}\right)\,dx,$$ execute: 20 | 21 | ```javascript 22 | var integrate = require('integrate-adaptive-simpson'); 23 | 24 | function f (x) { 25 | return Math.cos(1 / x) / x); 26 | } 27 | 28 | intiegrate(f, 0.01, 1, 1e-8); 29 | // => -0.3425527480294604 30 | ``` 31 | 32 | To integrate a vector function, you may import the vectorized version. To compute a contour integral of, say, $1 / z$ about $z_0 = 0$, that is, $$\oint \frac{dz}{z} = 2\pi i,$$ 33 | 34 | ```javascript 35 | var integrate = require('integrate-adaptive-simpson/vector'); 36 | 37 | integrate(function (f, theta) { 38 | // z = unit circle: 39 | var c = Math.cos(theta); 40 | var s = Math.sin(theta); 41 | 42 | // dz: 43 | var dzr = -s; 44 | var dzi = c; 45 | 46 | // 1 / z at this point on the unit circle: 47 | var fr = c / (c * c + s * s); 48 | var fi = -s / (c * c + s * s); 49 | 50 | // Multiply f(z) * dz: 51 | f[0] = fr * dzr - fi * dzi; 52 | f[1] = fr * dzi + fi * dzr; 53 | }, 0, Math.PI * 2); 54 | 55 | // => [ 0, 6.283185307179586 ] 56 | ``` 57 | 58 | ## API 59 | 60 | #### `require('integrate-adaptive-simpson')( f, a, b [, tol, maxdepth]] )` 61 | 62 | Compute the definite integral of scalar function f from a to b. 63 | 64 | **Arguments:** 65 | - `f`: The function to be integrated. A function of one variable that returns a value. 66 | - `a`: The lower limit of integration, $a$. 67 | - `b`: The upper limit of integration, $b$. 68 | - `tol`: The relative error required for an interval to be subdivided, based on Richardson extraplation. Default tolerance is `1e-8`. Be careful—the total accumulated error may be significantly less and result in more function evaluations than necessary. 69 | - `maxdepth`: The maximum recursion depth. Default depth is `20`. If reached, computation continues and a warning is output to the console. 70 | 71 | **Returns**: The computed value of the definite integral. 72 | 73 | #### `require('integrate-adaptive-simpson/vector')( f, a, b [, tol, maxdepth]] )` 74 | 75 | Compute the definite integral of vector function f from a to b. 76 | 77 | **Arguments:** 78 | - `f`: The function to be integrated. The first argument is an array of length `n` into which the output must be written. The second argument is the scalar value of the independent variable. 79 | - `a`: The lower limit of integration, $a$. 80 | - `b`: The upper limit of integration, $b$. 81 | - `tol`: The relative error required for an interval to be subdivided, based on Richardson extraplation. Default tolerance is `1e-8`. 82 | - `maxdepth`: The maximum recursion depth. Default depth is `20`. If reached, computation continues and a warning is output to the console. 83 | 84 | **Returns**: An `Array` representing The computed value of the definite integral. 85 | 86 | ## References 87 | Colins, C., [Romberg Integration and Adaptive Quadrature Course Notes](http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf). 88 | 89 | ## License 90 | 91 | (c) 2015 Scijs Authors. MIT License. 92 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Benchmark = require('scijs-benchmark'); 4 | var integrateVersion105 = require('integrate-adaptive-simpson'); 5 | var integrateCurrent = require('../'); 6 | var table = require('table').default; 7 | 8 | var bench = new Benchmark({ 9 | maxDuration: 1000, 10 | maxSamples: 1000, 11 | getTime: process.hrtime, 12 | getTimeDiff: function (t1, t2) { 13 | return (t2[0] - t1[0]) * 1e3 + (t2[1] - t1[1]) * 1e-6; 14 | } 15 | }); 16 | 17 | function f (x) { 18 | return Math.cos(1 / x) / x; 19 | } 20 | 21 | bench 22 | .measure('version 1.0.5', function () { 23 | integrateVersion105(f, 0.01, 1, 1e-10, 22); 24 | }) 25 | 26 | .measure('current version', function () { 27 | integrateCurrent(f, 0.01, 1, 1e-10, 22); 28 | }) 29 | 30 | .run(function (err, results) { 31 | if (err) { 32 | console.log('Error: ', err); 33 | } 34 | console.log(table(bench.toTable())); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmark", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "integrate-adaptive-simpson": "1.0.5", 13 | "scijs-benchmark": "git@github.com:rreusser/scijs-benchmark.git", 14 | "table": "^3.7.8" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/comparison.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var adsimp = require('../lib'); 4 | var simpson = require('integrate-simpson'); 5 | var value, evaluations; 6 | 7 | function f (x) { 8 | evaluations++; 9 | return Math.sin(x); 10 | } 11 | 12 | evaluations = 0; 13 | value = simpson(f, 0, Math.PI, 22); 14 | console.log('\nSimpson integration (' + evaluations + ' evaluations):'); 15 | console.log('Absolute error:', Math.abs(value - 2)); 16 | 17 | // Adaptive integration requires about half the number of evaluations 18 | // acheive the same absolute error: 19 | evaluations = 0; 20 | value = adsimp(f, 0, Math.PI, 1e-4); 21 | console.log('\nAdaptive Simpson integration (' + evaluations + ' evaluations):'); 22 | console.log('Absolute error:', Math.abs(value - 2)); 23 | 24 | // Adaptive integration with default tolerance: 25 | evaluations = 0; 26 | value = adsimp(f, 0, Math.PI); 27 | console.log('\nAdaptive simpson integration with defaults(' + evaluations + ' evaluations):'); 28 | console.log('Absolute error:', Math.abs(value - 2)); 29 | -------------------------------------------------------------------------------- /examples/mathjs.js: -------------------------------------------------------------------------------- 1 | // 2 | // In order to run this example, you'll need to run: 3 | // $ npm install mathjs 4 | // 5 | 6 | var mathjs = require('mathjs'); 7 | var integrate = require('../'); 8 | var parser = mathjs.parser(); 9 | 10 | parser.eval('f(x) = cos(1/x)/x'); 11 | var value = integrate(parser.get('f'), 0.01, 1, 0.001); 12 | 13 | console.log('integral of cos(1/x)/x from 0.01 to 1:'); 14 | console.log('expected approimxate value: -0.342553'); 15 | console.log('value = ', value); 16 | -------------------------------------------------------------------------------- /examples/oscillatory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var romberg = require('../'); 4 | var simpson = require('integrate-simpson'); 5 | var value, evaluations; 6 | 7 | function f (x) { 8 | evaluations++; 9 | return Math.cos(1 / x) / x; 10 | } 11 | 12 | // Calculated via wolfram alpha, using a roundabout way of getting a few extra digits of (hopeful) precision: 13 | // http://www.wolframalpha.com/input/?i=%28integral+of+%28cos%281%2Fx%29%2Fx%29+from+x%3D0.01+to+x%3D1%29+%2B+0.342553+-+2.51956e-7 14 | var actual = -0.34255274804359265; 15 | 16 | evaluations = 0; 17 | value = simpson(f, 0.01, 1, 29118); 18 | console.log('\nSimpson integration (' + evaluations + ' evaluations):'); 19 | console.log('Absolute error:', Math.abs(value - actual)); 20 | 21 | // Adaptive integration requires about half the number of evaluations 22 | // acheive the same absolute error: 23 | evaluations = 0; 24 | value = romberg(f, 0.01, 1, 3.1e-6); 25 | console.log('\nAdaptive Simpson integration (' + evaluations + ' evaluations):'); 26 | console.log('Absolute error:', Math.abs(value - actual)); 27 | -------------------------------------------------------------------------------- /examples/vector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var integrate = require('../vector'); 4 | 5 | var res = integrate(function (f, theta) { 6 | var c = Math.cos(theta); 7 | var s = Math.sin(theta); 8 | 9 | var dzr = -s; 10 | var dzi = c; 11 | 12 | var fr = c / (c * c + s * s); 13 | var fi = -s / (c * c + s * s); 14 | 15 | f[0] = fr * dzr - fi * dzi; 16 | f[1] = fr * dzi + fi * dzr; 17 | }, 0, Math.PI * 2); 18 | 19 | console.log('integral 1 / z about 0 =', res); 20 | 21 | -------------------------------------------------------------------------------- /images/1-z-32ebeece91.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/1-z-32ebeece91.png -------------------------------------------------------------------------------- /images/a-2217a6870d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/a-2217a6870d.png -------------------------------------------------------------------------------- /images/b-224c764dec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/b-224c764dec.png -------------------------------------------------------------------------------- /images/int_0011-frac1xcosleftfrac1xrightdx-c5d6a6f216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/int_0011-frac1xcosleftfrac1xrightdx-c5d6a6f216.png -------------------------------------------------------------------------------- /images/int_ab-fx-dx-49d001614b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/int_ab-fx-dx-49d001614b.png -------------------------------------------------------------------------------- /images/oint-fracdzz-2pi-i-3243136d9d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/oint-fracdzz-2pi-i-3243136d9d.png -------------------------------------------------------------------------------- /images/z_0-0-227c53dd15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scijs/integrate-adaptive-simpson/7795a57232536f2dee6b492b1194d36aa2437bf3/images/z_0-0-227c53dd15.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = integrate; 4 | 5 | // This algorithm adapted from pseudocode in: 6 | // http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf 7 | function adsimp (f, a, b, fa, fm, fb, V0, tol, maxdepth, depth, state) { 8 | if (state.nanEncountered) { 9 | return NaN; 10 | } 11 | 12 | var h, f1, f2, sl, sr, s2, m, V1, V2, err; 13 | 14 | h = b - a; 15 | f1 = f(a + h * 0.25); 16 | f2 = f(b - h * 0.25); 17 | 18 | // Simple check for NaN: 19 | if (isNaN(f1)) { 20 | state.nanEncountered = true; 21 | return; 22 | } 23 | 24 | // Simple check for NaN: 25 | if (isNaN(f2)) { 26 | state.nanEncountered = true; 27 | return; 28 | } 29 | 30 | sl = h * (fa + 4 * f1 + fm) / 12; 31 | sr = h * (fm + 4 * f2 + fb) / 12; 32 | s2 = sl + sr; 33 | err = (s2 - V0) / 15; 34 | 35 | if (depth > maxdepth) { 36 | state.maxDepthCount++; 37 | return s2 + err; 38 | } else if (Math.abs(err) < tol) { 39 | return s2 + err; 40 | } else { 41 | m = a + h * 0.5; 42 | 43 | V1 = adsimp(f, a, m, fa, f1, fm, sl, tol * 0.5, maxdepth, depth + 1, state); 44 | 45 | if (isNaN(V1)) { 46 | state.nanEncountered = true; 47 | return NaN; 48 | } 49 | 50 | V2 = adsimp(f, m, b, fm, f2, fb, sr, tol * 0.5, maxdepth, depth + 1, state); 51 | 52 | if (isNaN(V2)) { 53 | state.nanEncountered = true; 54 | return NaN; 55 | } 56 | 57 | return V1 + V2; 58 | } 59 | } 60 | 61 | function integrate (f, a, b, tol, maxdepth) { 62 | var state = { 63 | maxDepthCount: 0, 64 | nanEncountered: false 65 | }; 66 | 67 | if (tol === undefined) { 68 | tol = 1e-8; 69 | } 70 | if (maxdepth === undefined) { 71 | maxdepth = 20; 72 | } 73 | 74 | var fa = f(a); 75 | var fm = f(0.5 * (a + b)); 76 | var fb = f(b); 77 | 78 | var V0 = (fa + 4 * fm + fb) * (b - a) / 6; 79 | 80 | var result = adsimp(f, a, b, fa, fm, fb, V0, tol, maxdepth, 1, state); 81 | 82 | if (state.maxDepthCount > 0 && console && console.warn) { 83 | console.warn('integrate-adaptive-simpson: Warning: maximum recursion depth (' + maxdepth + ') reached ' + state.maxDepthCount + ' times'); 84 | } 85 | 86 | if (state.nanEncountered && console && console.warn) { 87 | console.warn('integrate-adaptive-simpson: Warning: NaN encountered. Halting early.'); 88 | } 89 | 90 | return result; 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integrate-adaptive-simpson", 3 | "version": "1.1.1", 4 | "description": "Integrate a system of ODEs using the Second Order Runge-Kutta (Midpoint) method", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "semistandard && mocha", 8 | "lint": "semistandard" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/scijs/integrate-adaptive-simpson.git" 13 | }, 14 | "keywords": [ 15 | "scijs", 16 | "integral", 17 | "definite-integral", 18 | "integration", 19 | "calculus", 20 | "adaptive", 21 | "romberg" 22 | ], 23 | "author": "Ricky Reusser", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "almost-equal": "^1.1.0", 27 | "chai": "^3.0.0", 28 | "integrate-simpson": "^1.0.3", 29 | "mocha": "^2.2.5", 30 | "semistandard": "^7.0.5", 31 | "sinon": "^1.17.3" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* globals it, describe */ 2 | 'use strict'; 3 | 4 | var adaptiveSimpson = require('../'); 5 | var assert = require('chai').assert; 6 | var sinon = require('sinon'); 7 | // var simpson = require('integrate-simpson'); 8 | 9 | describe('Adaptive Simpson integration', function () { 10 | it('integrates a constant exactly', function () { 11 | var evaluations = 0; 12 | var value = adaptiveSimpson(function () { 13 | evaluations++; 14 | return 2; 15 | }, 1, 3); 16 | 17 | assert.closeTo(value, 4, 0, '= 4'); 18 | 19 | // Five evaluations means exact evaluation + error estimate = 0: 20 | assert.equal(evaluations, 5, 'evaluates the function five times'); 21 | }); 22 | 23 | it('integrates a cubic exactly', function () { 24 | var evaluations = 0; 25 | var value = adaptiveSimpson(function (x) { 26 | evaluations++; 27 | return x * x * x - x * x; 28 | }, 1, 3); 29 | 30 | assert.closeTo(value, 34 / 3, 1e-10, '= 34/3'); 31 | 32 | // Five evaluations means exact evaluation + error estimate = 0: 33 | assert.equal(evaluations, 5, 'evaluates the function five times'); 34 | }); 35 | 36 | it('integrates an irrational function', function () { 37 | var evaluations = 0; 38 | var f = function (x) { 39 | evaluations++; 40 | return Math.sin(x); 41 | }; 42 | var value = adaptiveSimpson(f, 0, Math.PI); 43 | assert.closeTo(value, 2, 1e-10, '= 2'); 44 | }); 45 | 46 | it('stops immediately when NaN encountered', function () { 47 | var nanEvaluations = 0; 48 | var f = function (x) { 49 | if (x > 0.2 && x < 0.3) { 50 | nanEvaluations++; 51 | return NaN; 52 | } 53 | return Math.sin(x); 54 | }; 55 | var value = adaptiveSimpson(f, 0, Math.PI); 56 | assert.equal(nanEvaluations, 1); 57 | assert.isNaN(value); 58 | }); 59 | 60 | it('stops quickly when infinity encountered', function () { 61 | // Inifnity carries through to NaN pretty quickly, so we'll avoid a separate 62 | // check, but it will bail pretty quickly: 63 | var infinityEvaluations = 0; 64 | var f = function (x) { 65 | if (x > 0.2 && x < 0.3) { 66 | infinityEvaluations++; 67 | return Infinity; 68 | } 69 | return Math.sin(x); 70 | }; 71 | var value = adaptiveSimpson(f, 0, Math.PI, 1e-10, 5); 72 | 73 | // This would be like 100000 if not caught: 74 | assert(infinityEvaluations < 10); 75 | assert.isNaN(value); 76 | }); 77 | 78 | it('integrates an oscillatory function', function () { 79 | // See: http://www.wolframalpha.com/input/?i=integrate+cos%281%2Fx%29%2Fx+from+x%3D0.01+to+x%3D1 80 | var evaluations = 0; 81 | var f = function (x) { 82 | evaluations++; 83 | return Math.cos(1 / x) / x; 84 | }; 85 | 86 | // Selected this tolerance with trial and error to minimize evaluations: 87 | var v1 = adaptiveSimpson(f, 0.01, 1, 6e-4, 20); 88 | assert.closeTo(v1, -0.342553, 1e-5, '= 2'); 89 | }); 90 | 91 | it('integrates 3 * x + 10 from 0 to 10', function () { 92 | var g = function (x) { 93 | return 3 * x + 10; 94 | }; 95 | var value = adaptiveSimpson(g, 0, 10, 1); 96 | assert.closeTo(value, 250, 1e-5, '= 250'); 97 | 98 | value = adaptiveSimpson(g, 0, 10, 0.9); 99 | assert.closeTo(value, 250, 1e-5, '= 250'); 100 | }); 101 | 102 | it('Prints one console warn when tolerance exceeded', function () { 103 | var _origWarn = console.warn; 104 | console.warn = sinon.spy(_origWarn); 105 | 106 | var f = function (x) { 107 | return Math.cos(1 / x) / x; 108 | }; 109 | 110 | // Selected this tolerance with trial and error to minimize evaluations: 111 | var v1 = adaptiveSimpson(f, 0.01, 1, 0, 15); 112 | assert.closeTo(v1, -0.342553, 1e-3, '= 2'); 113 | assert.equal(console.warn.callCount, 1, 'console.warn called once'); 114 | 115 | console.warn = _origWarn; 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/util/array-equal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var almostEqual = require('almost-equal'); 4 | 5 | module.exports = arrayEqual; 6 | 7 | function arrayEqual (computed, expected, t1, t2) { 8 | t1 = t1 === undefined ? almostEqual.FLT_EPSILON : t1; 9 | t2 = t2 === undefined ? almostEqual.FLT_EPSILON : t2; 10 | 11 | if (computed.length !== expected.length) { 12 | throw new Error('Length of computed (' + computed.length + ') not equal to length of expected (' + expected.length + ')'); 13 | } 14 | 15 | for (var i = 0; i < computed.length; i++) { 16 | if (!almostEqual(computed[i], expected[i], t1, t2)) { 17 | throw new Error('Arrays not equal. computed[' + i + '] !~ expected[' + i + '] (' + computed[i] + ' !~ ' + expected[i] + ')'); 18 | } 19 | } 20 | 21 | return true; 22 | } 23 | -------------------------------------------------------------------------------- /test/util/contour-integrand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = contourIntegrand; 4 | 5 | function contourIntegrand (f, z0r, z0i, r) { 6 | // Defaults: 7 | r = r === undefined ? 1 : r; 8 | z0r = z0r === undefined ? 0 : z0r; 9 | z0i = z0i === undefined ? 0 : z0i; 10 | 11 | if (f.length === 2) { 12 | return function (out, theta) { 13 | // Compute the unit circle: 14 | var c = Math.cos(theta); 15 | var s = Math.sin(theta); 16 | var dzr = -r * s; 17 | var dzi = r * c; 18 | var a = z0r + r * c; 19 | var b = z0i + r * s; 20 | 21 | // Evaluate the function: 22 | var y = f(a, b); 23 | 24 | // Compute the integrand: 25 | out[0] = y[0] * dzr - y[1] * dzi; 26 | out[1] = y[0] * dzi + y[1] * dzr; 27 | }; 28 | } else if (f.length === 3) { 29 | // Avoid allocating storage over and over: 30 | var y = []; 31 | 32 | return function (out, theta) { 33 | // Compute the unit circle: 34 | var c = Math.cos(theta); 35 | var s = Math.sin(theta); 36 | var dzr = -r * s; 37 | var dzi = r * c; 38 | var a = z0r + r * c; 39 | var b = z0i + r * s; 40 | 41 | // Evaluate the function: 42 | f(y, a, b); 43 | 44 | // Compute the integrand: 45 | out[0] = y[0] * dzr - y[1] * dzi; 46 | out[1] = y[0] * dzi + y[1] * dzr; 47 | }; 48 | } else { 49 | throw new Error('f must be a function accepting either two or three arguments'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/vector.js: -------------------------------------------------------------------------------- 1 | /* globals it, describe */ 2 | 'use strict'; 3 | 4 | var integrate = require('../vector'); 5 | var assert = require('chai').assert; 6 | var arrayEqual = require('./util/array-equal'); 7 | var contourIntegrand = require('./util/contour-integrand'); 8 | 9 | describe('Vector adaptive Simpson integration', function () { 10 | it('integrates f(z) = 1 + i around the unit complex circle', function () { 11 | var result = integrate(contourIntegrand(function (a, b) { 12 | return [1, 1]; 13 | }), 0, Math.PI * 2); 14 | 15 | assert(arrayEqual(result, [0, 0])); 16 | }); 17 | 18 | it('integrates f(z) = z around the unit complex circle', function () { 19 | var result = integrate(contourIntegrand(function (a, b) { 20 | return [a, b]; 21 | }), 0, Math.PI * 2); 22 | 23 | assert(arrayEqual(result, [0, 0])); 24 | }); 25 | 26 | it('integrates f(z) = z^2 around the unit complex circle', function () { 27 | var result = integrate(contourIntegrand(function (a, b) { 28 | return [a * a - b * b, 2 * a * b]; 29 | }), 0, Math.PI * 2); 30 | 31 | assert(arrayEqual(result, [0, 0])); 32 | }); 33 | 34 | it('integrates f(z) = z^2 around the unit complex circle', function () { 35 | var result = integrate(contourIntegrand(function (a, b) { 36 | return [a * a - b * b, 2 * a * b]; 37 | }), 0, Math.PI * 2); 38 | 39 | assert(arrayEqual(result, [0, 0])); 40 | }); 41 | 42 | it('computes Res 1 / z about z0 = 0', function () { 43 | var result = integrate(contourIntegrand(function (a, b) { 44 | var denom = a * a + b * b; 45 | return [a / denom, -b / denom]; 46 | }), 0, Math.PI * 2); 47 | 48 | assert(arrayEqual(result, [0, Math.PI * 2])); 49 | }); 50 | 51 | it('computes Res 1 / z about z0 = 10 + 10i', function () { 52 | var result = integrate(contourIntegrand(function (f, a, b) { 53 | var denom = a * a + b * b; 54 | f[0] = a / denom; 55 | f[1] = -b / denom; 56 | }, 2, 2, 1), 0, Math.PI * 2); 57 | 58 | assert(arrayEqual(result, [0, 0])); 59 | }); 60 | 61 | it('computes Res 1 / (z - 10 + 10i) about z0 = 0', function () { 62 | var result = integrate(contourIntegrand(function (a, b) { 63 | var denom = a * a + b * b; 64 | return [a / denom, -b / denom]; 65 | }, 10, -10, 1), 0, Math.PI * 2); 66 | 67 | assert(arrayEqual(result, [0, 0])); 68 | }); 69 | 70 | it('computes Res 1 / (z - 10 + 10i) about z0 = 10 - 10i', function () { 71 | var result = integrate(contourIntegrand(function (a, b) { 72 | var a0 = a - 10; 73 | var b0 = b + 10; 74 | var denom = a0 * a0 + b0 * b0; 75 | return [a0 / denom, -b0 / denom]; 76 | }, 10, -10, 1), 0, Math.PI * 2); 77 | 78 | assert(arrayEqual(result, [0, Math.PI * 2])); 79 | }); 80 | 81 | it('computes Res 1 / (z - 10 + 10i) about z0 = 10 - 10i (radius = 2)', function () { 82 | var result = integrate(contourIntegrand(function (a, b) { 83 | var a0 = a - 10; 84 | var b0 = b + 10; 85 | var denom = a0 * a0 + b0 * b0; 86 | return [a0 / denom, -b0 / denom]; 87 | }, 10, -10, 2), 0, Math.PI * 2); 88 | 89 | assert(arrayEqual(result, [0, Math.PI * 2])); 90 | }); 91 | 92 | it('computes Res 1 / z^2 about z0 = 0', function () { 93 | var result = integrate(contourIntegrand(function (a, b) { 94 | var denom = Math.pow(a * a + b * b, 2); 95 | return [ 96 | (a * a - b * b) / denom, 97 | -2 * a * b / denom 98 | ]; 99 | }), 0, Math.PI * 2); 100 | 101 | assert(arrayEqual(result, [0, 0])); 102 | }); 103 | 104 | it('computes Res 1 / z^3 about z0 = 0', function () { 105 | var result = integrate(contourIntegrand(function (a, b) { 106 | var denom = Math.pow(a * a + b * b, 3); 107 | return [ 108 | (a * a * a - 3 * a * b * b) / denom, 109 | (b * b * b - 3 * a * a * b) / denom 110 | ]; 111 | }), 0, Math.PI * 2); 112 | 113 | assert(arrayEqual(result, [0, 0])); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /vector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = integrate; 4 | 5 | // This algorithm adapted from pseudocode in: 6 | // http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf 7 | function adsimp (out, f, a, b, n, fa, fm, fb, V0, tol, maxdepth, depth, state) { 8 | var i; 9 | if (state.nanEncountered) { 10 | return NaN; 11 | } 12 | 13 | var h, f1, f2, sl, sr, m, V1, V2, l2err; 14 | 15 | sl = new Array(n); 16 | sr = new Array(n); 17 | 18 | h = b - a; 19 | f1 = new Array(n); 20 | f2 = new Array(n); 21 | 22 | f(f1, a + h * 0.25); 23 | f(f2, b - h * 0.25); 24 | 25 | l2err = 0; 26 | for (i = 0; i < n; i++) { 27 | // Simple check for NaN: 28 | if (isNaN(f1[i])) { 29 | state.nanEncountered = true; 30 | return; 31 | } 32 | 33 | // Simple check for NaN: 34 | if (isNaN(f2[i])) { 35 | state.nanEncountered = true; 36 | return; 37 | } 38 | 39 | sl[i] = h * (fa[i] + 4 * f1[i] + fm[i]) / 12; 40 | sr[i] = h * (fm[i] + 4 * f2[i] + fb[i]) / 12; 41 | state.s2[i] = sl[i] + sr[i]; 42 | state.err[i] = Math.pow((state.s2[i] - V0[i]) / 15, 2); 43 | l2err += state.err[i]; 44 | } 45 | 46 | l2err = Math.sqrt(l2err); 47 | 48 | if (depth > maxdepth) { 49 | state.maxDepthCount++; 50 | for (i = 0; i < n; i++) { 51 | out[i] = state.s2[i] + state.err[i]; 52 | } 53 | } else if (l2err < tol) { 54 | for (i = 0; i < n; i++) { 55 | out[i] = state.s2[i] + state.err[i]; 56 | } 57 | } else { 58 | m = a + h * 0.5; 59 | 60 | V1 = new Array(n); 61 | adsimp(V1, f, a, m, n, fa, f1, fm, sl, tol * 0.5, maxdepth, depth + 1, state); 62 | 63 | for (i = 0; i < n; i++) { 64 | if (isNaN(V1[i])) { 65 | state.nanEncountered = true; 66 | return NaN; 67 | } 68 | } 69 | 70 | V2 = new Array(n); 71 | adsimp(V2, f, m, b, n, fm, f2, fb, sr, tol * 0.5, maxdepth, depth + 1, state); 72 | 73 | for (i = 0; i < n; i++) { 74 | if (isNaN(V2[i])) { 75 | state.nanEncountered = true; 76 | return NaN; 77 | } 78 | } 79 | 80 | for (i = 0; i < n; i++) { 81 | out[i] = V1[i] + V2[i]; 82 | } 83 | } 84 | } 85 | 86 | function integrate (f, a, b, tol, maxdepth) { 87 | var i, n, result; 88 | 89 | var state = { 90 | maxDepthCount: 0, 91 | nanEncountered: false 92 | }; 93 | 94 | if (tol === undefined) { 95 | tol = 1e-8; 96 | } 97 | if (maxdepth === undefined) { 98 | maxdepth = 20; 99 | } 100 | 101 | var fa = []; 102 | var fm = []; 103 | var fb = []; 104 | 105 | f(fa, a); 106 | f(fm, 0.5 * (a + b)); 107 | f(fb, b); 108 | 109 | // Get the dimensionality based on function evaluation: 110 | n = fa.length; 111 | 112 | // Avoid a bit of garbage collection. Could do way better, but this 113 | // is a start... 114 | state.err = []; 115 | state.s2 = []; 116 | 117 | var V0 = []; 118 | for (i = 0; i < n; i++) { 119 | V0[i] = (fa[i] + 4 * fm[i] + fb[i]) * (b - a) / 6; 120 | } 121 | 122 | result = []; 123 | for (i = 0; i < n; i++) { 124 | result[i] = 0; 125 | } 126 | 127 | adsimp(result, f, a, b, n, fa, fm, fb, V0, tol, maxdepth, 1, state); 128 | 129 | if (state.maxDepthCount > 0 && console && console.warn) { 130 | console.warn('integrate-adaptive-simpson: Warning: maximum recursion depth (' + maxdepth + ') reached ' + state.maxDepthCount + ' times'); 131 | } 132 | 133 | if (state.nanEncountered && console && console.warn) { 134 | console.warn('integrate-adaptive-simpson: Warning: NaN encountered. Halting early.'); 135 | } 136 | 137 | return result; 138 | } 139 | --------------------------------------------------------------------------------