├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── odex.js ├── odex.ts └── tonic-example.js ├── test ├── odexTest.js └── odexTest.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/a.out 2 | *.js.map 3 | .idea/ 4 | .vscode/ 5 | coverage/ 6 | node_modules/ 7 | typings/ 8 | *.iml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.5" 4 | - "7.10" 5 | before_script: 6 | - npm install -g istanbul 7 | after_success: 8 | - npm run coveralls 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Colin Smith 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## odex-js : ODEX in JavaScript 2 | [![Build Status](https://travis-ci.org/littleredcomputer/odex-js.svg?branch=master)](https://travis-ci.org/littleredcomputer/odex-js) [![GitHub license](https://img.shields.io/github/license/littleredcomputer/odex-js.svg)]() [![npm](https://img.shields.io/npm/v/odex.svg)]() [![Coverage Status](https://coveralls.io/repos/github/littleredcomputer/odex-js/badge.svg?branch=master)](https://coveralls.io/github/littleredcomputer/odex-js?branch=master) 3 | 4 | #### Numerically solves of non-stiff systems of ordinary differential equations in JavaScript. 5 | 6 | This is a port to JavaScript (actually, TypeScript) of [E. Hairer and 7 | G. Wanner's implementation][odex] of the [Gragg-Bulirsch-Stoer][gbs] method of integrating 8 | systems of differential equations. The original code is written in idiomatic 9 | Fortran; this code tries to present an idiomatic JavaScript interface while 10 | preserving all the of the virtues of the original code, including its speed, 11 | configurability, and compact memory footprint. 12 | 13 | #### Examples 14 | (We'll write the usage examples in plain JavaScript. The usage from TypeScript 15 | is very similar.) 16 | ##### One first-order equation 17 | 18 | The simplest possible example would be y′ = y, with y(0) = 1: we expect the 19 | solution y(x) = exp(x). First we create a solver object, telling how many 20 | independent variables there are in the system (in this case just one). 21 | 22 | ```js 23 | var odex = require('odex'); 24 | var s = new odex.Solver(1); 25 | ``` 26 | 27 | To represent the differential equation, we write a 28 | routine that computes y′ given y at the point x. For this example it's very 29 | simple: 30 | 31 | ```js 32 | var f = function(x, y) { 33 | return y; 34 | } 35 | ``` 36 | 37 | Since we asked for one independent variable, `y` is an array of length 1. 38 | We return an array of the same size. 39 | 40 | 41 | We can solve the equation by supplying the initial data and the start 42 | and endpoints. Let's find y(1): 43 | 44 | ```js 45 | s.solve(f, 46 | 0, // initial x value 47 | [1], // initial y values (just one in this example) 48 | 1); // final x value 49 | // { y: [ 2.7182817799042955 ], 50 | // outcome: 0, 51 | // nStep: 7, 52 | // xEnd: 1, 53 | // nAccept: 7, 54 | // nReject: 0, 55 | // nEval: 75 } 56 | ``` 57 | 58 | Not bad: the answer `y[1]` is close to *e*. It would be closer if we requested 59 | more precision. When you create a new `Solver` object, it is 60 | equipped with a number of properties you can change to control the integration. 61 | You can change a property and re-run the solution: 62 | 63 | ```js 64 | s.absoluteTolerance = s.relativeTolerance = 1e-10; 65 | s.solve(f, 0, [1], 1).y 66 | // [ 2.7182818284562535 ] 67 | Math.exp(1) - 2.7182818284562535 68 | // 2.7915447731174936e-12 69 | ``` 70 | 71 | ##### Integration callback 72 | You can supply a callback function that runs during the integration to supply intermediate 73 | points of the integration as it proceeds. The callback function is an optional 74 | parameter to `solve`, which receives the step number, x0, x1 and y(x1). (x0 75 | and x1 represent the interval covered in this integration step). 76 | 77 | ```js 78 | s.solve(f, 0, [1], 1, function(n,x0,x1,y) { 79 | console.log(n,x0,x1,y); 80 | }).y 81 | // 1 0 0 [ 1 ] 82 | // 2 0 0.0001 [ 1.0001000050001667 ] 83 | // 3 0.0001 0.0007841772783189289 [ 1.000784484825706 ] 84 | // 4 0.0007841772783189289 0.004832938716978181 [ 1.0048446362021166 ] 85 | // 5 0.004832938716978181 0.01913478583589434 [ 1.0193190291261103 ] 86 | // 6 0.01913478583589434 0.0937117110731088 [ 1.0982430889534374 ] 87 | // 7 0.0937117110731088 0.2862232977213724 [ 1.3313897183518322 ] 88 | // 8 0.2862232977213724 0.7103628434248046 [ 2.034729412908106 ] 89 | // 9 0.7103628434248046 1 [ 2.7182818284562535 ] 90 | // [ 2.7182818284562535 ] 91 | ``` 92 | 93 | You will observe that `odex` has chosen its own grid points for evaluation. 94 | Adaptive step size is one of the nicest features of this library: you don't 95 | have to worry about it too much. 96 | 97 | ##### Dense Output 98 | However, you will often want to sample the data at points of your own choosing. 99 | When you request `denseOutput` in the `Solver` parameters, the function you 100 | supply to solve receives a fifth argument which is a closure which you can call to obtain 101 | very accurate y values in the interval [x0, x1]. You call this closure with 102 | the index (within the y vector) of the component you want to evaluate, and the 103 | x value in [x0, x1] where you want to find that y value. One common use case 104 | for this is to obtain otuput at evenly spaced points. To this end, we supply a 105 | canned callback `grid` which you can use for this: 106 | 107 | ```js 108 | s.denseOutput = true; // request interpolation closure in solution callback 109 | s.solve(f, 0, [1], 1, s.grid(0.2, function(x,y) { 110 | console.log(x,y); 111 | })); 112 | // 0 [ 1 ] 113 | // 0.2 [ 1.2214027470178732 ] 114 | // 0.4 [ 1.4918240050068732 ] 115 | // 0.6 [ 1.8221161568592519 ] 116 | // 0.8 [ 2.2255378426172316 ] 117 | // 1 [ 2.7182804587510203 ] 118 | // [ 2.7182804587510203 ] 119 | ``` 120 | 121 | To see how you could use the dense output feature yourself, take a look at 122 | the source to grid. 123 | ##### A system of two first order equations 124 | Note that in all these examples, `y` is a vector: this software is designed to 125 | solve systems. Let's work with the [Lotka-Volterra][lv] predator-prey system. 126 | The system is: 127 | 128 | ``` 129 | dx/dt = a x - b x y 130 | dy/dt = c x y - d y 131 | ``` 132 | 133 | For odex, we rename *t* to *x*, and then *x* and *y* become `y[0]` and `y[1]`. 134 | We write a function LV which binds the constants of the population system 135 | `a`, `b`, `c`, `d` and returns a function suitable for the integrator. 136 | To represent this system we can write: 137 | 138 | ```js 139 | var LotkaVolterra = function(a, b, c, d) { 140 | return function(x, y) { 141 | return [ 142 | a * y[0] - b * y[0] * y[1], 143 | c * y[0] * y[1] - d * y[1] 144 | ]; 145 | }; 146 | }; 147 | ``` 148 | 149 | Then we can solve it. It's the same as the previous examples, but this time 150 | we need a solver created to handle two independent variables and must supply 151 | initial data for both of them. To find the state of the rabbits and wolves 152 | at time 6, if the state at time zero is {y0 = 1, y1 153 | = 1}: 154 | 155 | ```js 156 | s = new odex.Solver(2); 157 | s.solve(LotkaVolterra(2/3, 4/3, 1, 1), 0, [1, 1], 6).y 158 | // [ 1.6542774481418214, 0.3252864486771545 ] 159 | ```` 160 | To see more of this system of equations in action, you can visit a 161 | [demo page][lvdemo] which allows you to vary the initial conditions 162 | with the mouse. 163 | 164 | ##### A second-order equation 165 | 166 | You can integrate second order ordinary differential equations by making a 167 | simple transformation to a system of first order equations. Consider 168 | [Airy's equation][airy]: y″ − x y = 0: 169 | 170 | In ODEX, we could write y0 for y and y1 for y′, 171 | so that y″ = y′1 and rewrite the system like this: 172 | y′0 = y1;  173 | y′1 − x y0 = 0 to get: 174 | 175 | ```js 176 | var airy = function(x, y) { 177 | return [y[1], x * y[0]]; 178 | } 179 | ``` 180 | There's also a [demo page][airydemo] for this equation too. 181 | 182 | You might also enjoy a demo of the [Lorenz attractor][lorenz] or 183 | [Van der Pol equation][vanderpol]! 184 | 185 | #### Tests 186 | This project comes with a mocha test suite. The suite contains other 187 | examples of second-order equations which have been translated to 188 | systems of first order equations you may examine. 189 | 190 | [odex]: http://www.unige.ch/~hairer/software.html 191 | [gbs]: https://en.wikipedia.org/wiki/Bulirsch%E2%80%93Stoer_algorithm 192 | [lv]: https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations 193 | [lvdemo]: http://blog.littleredcomputer.net/math/odex/js/2016/04/03/lotka-volterra.html 194 | [airy]: https://en.wikipedia.org/wiki/Airy_function 195 | [airydemo]: http://blog.littleredcomputer.net/jekyll/update/2016/04/03/diffeq-javascript.html 196 | [lorenz]: http://blog.littleredcomputer.net/math/odex/js/2016/04/03/lorenz-attractor.html 197 | [vanderpol]: http://blog.littleredcomputer.net/math/odex/js/2016/04/20/van-der-pol.html 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odex", 3 | "version": "2.0.4", 4 | "description": "Ernst Hairer's ODEX nonstiff ODE solver in JavaScript", 5 | "keywords": [ 6 | "ordinary", 7 | "differential", 8 | "equation", 9 | "numerical", 10 | "method", 11 | "ode", 12 | "solver" 13 | ], 14 | "main": "src/odex.js", 15 | "scripts": { 16 | "test": "mocha test", 17 | "test2": "mocha --require intelli-espower-loader test", 18 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha test", 19 | "coveralls": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js && rm -rf coverage" 20 | }, 21 | "repository": { 22 | "url": "http://github.com/littleredcomputer/odex-js" 23 | }, 24 | "author": "Colin Smith (http://github.com/littleredcomputer)", 25 | "license": "BSD-2-Clause", 26 | "devDependencies": { 27 | "@types/jasmine": "^2.5.47", 28 | "@types/power-assert": "^1.4.29", 29 | "coveralls": "^2.13.1", 30 | "intelli-espower-loader": "^1.0.1", 31 | "jasmine": "^2.6.0", 32 | "mocha": "^3.4.1", 33 | "mocha-lcov-reporter": "^1.3.0", 34 | "power-assert": "^1.3.1" 35 | }, 36 | "tonicExampleFilename": "src/tonic-example.js" 37 | } 38 | -------------------------------------------------------------------------------- /src/odex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * An implementation of ODEX, by E. Hairer and G. Wanner, ported from the Fortran ODEX.F. 4 | * The original work carries the BSD 2-clause license, and so does this. 5 | * 6 | * Copyright (c) 2016 Colin Smith. 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | * disclaimer. 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 10 | * following disclaimer in the documentation and/or other materials provided with the distribution. 11 | * 12 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 13 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 14 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 15 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 16 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 17 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 18 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | */ 20 | Object.defineProperty(exports, "__esModule", { value: true }); 21 | var Outcome; 22 | (function (Outcome) { 23 | Outcome[Outcome["Converged"] = 0] = "Converged"; 24 | Outcome[Outcome["MaxStepsExceeded"] = 1] = "MaxStepsExceeded"; 25 | Outcome[Outcome["EarlyReturn"] = 2] = "EarlyReturn"; 26 | })(Outcome = exports.Outcome || (exports.Outcome = {})); 27 | var Solver = (function () { 28 | function Solver(n) { 29 | this.n = n; 30 | this.uRound = 2.3e-16; 31 | this.maxSteps = 10000; 32 | this.initialStepSize = 1e-4; 33 | this.maxStepSize = 0; 34 | this.maxExtrapolationColumns = 9; 35 | this.stepSizeSequence = 0; 36 | this.stabilityCheckCount = 1; 37 | this.stabilityCheckTableLines = 2; 38 | this.denseOutput = false; 39 | this.denseOutputErrorEstimator = true; 40 | this.denseComponents = undefined; 41 | this.interpolationFormulaDegree = 4; 42 | this.stepSizeReductionFactor = 0.5; 43 | this.stepSizeFac1 = 0.02; 44 | this.stepSizeFac2 = 4.0; 45 | this.stepSizeFac3 = 0.8; 46 | this.stepSizeFac4 = 0.9; 47 | this.stepSafetyFactor1 = 0.65; 48 | this.stepSafetyFactor2 = 0.94; 49 | this.relativeTolerance = 1e-5; 50 | this.absoluteTolerance = 1e-5; 51 | this.debug = false; 52 | } 53 | Solver.prototype.grid = function (dt, out) { 54 | if (!this.denseOutput) 55 | throw new Error('Must set .denseOutput to true when using grid'); 56 | var components = this.denseComponents; 57 | if (!components) { 58 | components = []; 59 | for (var i = 0; i < this.n; ++i) 60 | components.push(i); 61 | } 62 | var t; 63 | return function (n, xOld, x, y, interpolate) { 64 | if (n === 1) { 65 | var v = out(x, y); 66 | t = x + dt; 67 | return v; 68 | } 69 | while (t <= x) { 70 | var yf = []; 71 | for (var _i = 0, components_1 = components; _i < components_1.length; _i++) { 72 | var i = components_1[_i]; 73 | yf.push(interpolate(i, t)); 74 | } 75 | var v = out(t, yf); 76 | if (v === false) 77 | return false; 78 | t += dt; 79 | } 80 | }; 81 | }; 82 | // Make a 1-based 2D array, with r rows and c columns. The initial values are undefined. 83 | Solver.dim2 = function (r, c) { 84 | var a = new Array(r + 1); 85 | for (var i = 1; i <= r; ++i) 86 | a[i] = Solver.dim(c); 87 | return a; 88 | }; 89 | // Generate step size sequence and return as a 1-based array of length n. 90 | Solver.stepSizeSequence = function (nSeq, n) { 91 | var a = new Array(n + 1); 92 | a[0] = 0; 93 | switch (nSeq) { 94 | case 1: 95 | for (var i = 1; i <= n; ++i) 96 | a[i] = 2 * i; 97 | break; 98 | case 2: 99 | a[1] = 2; 100 | for (var i = 2; i <= n; ++i) 101 | a[i] = 4 * i - 4; 102 | break; 103 | case 3: 104 | a[1] = 2; 105 | a[2] = 4; 106 | a[3] = 6; 107 | for (var i = 4; i <= n; ++i) 108 | a[i] = 2 * a[i - 2]; 109 | break; 110 | case 4: 111 | for (var i = 1; i <= n; ++i) 112 | a[i] = 4 * i - 2; 113 | break; 114 | case 5: 115 | for (var i = 1; i <= n; ++i) 116 | a[i] = 4 * i; 117 | break; 118 | default: 119 | throw new Error('invalid stepSizeSequence selected'); 120 | } 121 | return a; 122 | }; 123 | // Integrate the differential system represented by f, from x to xEnd, with initial data y. 124 | // solOut, if provided, is called at each integration step. 125 | Solver.prototype.solve = function (f, x, y0, xEnd, solOut) { 126 | var _this = this; 127 | // Make a copy of y0, 1-based. We leave the user's parameters alone so that they may be reused if desired. 128 | var y = [0].concat(y0); 129 | var dz = Solver.dim(this.n); 130 | var yh1 = Solver.dim(this.n); 131 | var yh2 = Solver.dim(this.n); 132 | if (this.maxSteps <= 0) 133 | throw new Error('maxSteps must be positive'); 134 | var km = this.maxExtrapolationColumns; 135 | if (km <= 2) 136 | throw new Error('maxExtrapolationColumns must be > 2'); 137 | var nSeq = this.stepSizeSequence || (this.denseOutput ? 4 : 1); 138 | if (nSeq <= 3 && this.denseOutput) 139 | throw new Error('stepSizeSequence incompatible with denseOutput'); 140 | if (this.denseOutput && !solOut) 141 | throw new Error('denseOutput requires a solution observer function'); 142 | if (this.interpolationFormulaDegree <= 0 || this.interpolationFormulaDegree >= 7) 143 | throw new Error('bad interpolationFormulaDegree'); 144 | var icom = [0]; // icom will be 1-based, so start with a pad entry. 145 | var nrdens = 0; 146 | if (this.denseOutput) { 147 | if (this.denseComponents) { 148 | for (var _i = 0, _a = this.denseComponents; _i < _a.length; _i++) { 149 | var c = _a[_i]; 150 | // convert dense components requested into one-based indexing. 151 | if (c < 0 || c > this.n) 152 | throw new Error('bad dense component: ' + c); 153 | icom.push(c + 1); 154 | ++nrdens; 155 | } 156 | } 157 | else { 158 | // if user asked for dense output but did not specify any denseComponents, 159 | // request all of them. 160 | for (var i = 1; i <= this.n; ++i) { 161 | icom.push(i); 162 | } 163 | nrdens = this.n; 164 | } 165 | } 166 | if (this.uRound <= 1e-35 || this.uRound > 1) 167 | throw new Error('suspicious value of uRound'); 168 | var hMax = Math.abs(this.maxStepSize || xEnd - x); 169 | var lfSafe = 2 * km * km + km; 170 | function expandToArray(x, n) { 171 | // If x is an array, return a 1-based copy of it. If x is a number, return a new 1-based array 172 | // consisting of n copies of the number. 173 | var tolArray = [0]; 174 | if (Array.isArray(x)) { 175 | return tolArray.concat(x); 176 | } 177 | else { 178 | for (var i = 0; i < n; ++i) 179 | tolArray.push(x); 180 | return tolArray; 181 | } 182 | } 183 | var aTol = expandToArray(this.absoluteTolerance, this.n); 184 | var rTol = expandToArray(this.relativeTolerance, this.n); 185 | var _b = [0, 0, 0, 0], nEval = _b[0], nStep = _b[1], nAccept = _b[2], nReject = _b[3]; 186 | // call to core integrator 187 | var nrd = Math.max(1, nrdens); 188 | var ncom = Math.max(1, (2 * km + 5) * nrdens); 189 | var dens = Solver.dim(ncom); 190 | var fSafe = Solver.dim2(lfSafe, nrd); 191 | // Wrap f in a function F which hides the one-based indexing from the customers. 192 | var F = function (x, y, yp) { 193 | var ret = f(x, y.slice(1)); 194 | for (var i = 0; i < ret.length; ++i) 195 | yp[i + 1] = ret[i]; 196 | }; 197 | var odxcor = function () { 198 | // The following three variables are COMMON/CONTEX/ 199 | var xOldd; 200 | var hhh; 201 | var kmit; 202 | var acceptStep = function (n) { 203 | // Returns true if we should continue the integration. The only time false 204 | // is returned is when the user's solution observation function has returned false, 205 | // indicating that she does not wish to continue the computation. 206 | xOld = x; 207 | x += h; 208 | if (_this.denseOutput) { 209 | // kmit = mu of the paper 210 | kmit = 2 * kc - _this.interpolationFormulaDegree + 1; 211 | for (var i = 1; i <= nrd; ++i) 212 | dens[i] = y[icom[i]]; 213 | xOldd = xOld; 214 | hhh = h; // note: xOldd and hhh are part of /CONODX/ 215 | for (var i = 1; i <= nrd; ++i) 216 | dens[nrd + i] = h * dz[icom[i]]; 217 | var kln = 2 * nrd; 218 | for (var i = 1; i <= nrd; ++i) 219 | dens[kln + i] = t[1][icom[i]]; 220 | // compute solution at mid-point 221 | for (var j = 2; j <= kc; ++j) { 222 | var dblenj = nj[j]; 223 | for (var l = j; l >= 2; --l) { 224 | var factor = Math.pow((dblenj / nj[l - 1]), 2) - 1; 225 | for (var i = 1; i <= nrd; ++i) { 226 | ySafe[l - 1][i] = ySafe[l][i] + (ySafe[l][i] - ySafe[l - 1][i]) / factor; 227 | } 228 | } 229 | } 230 | var krn = 4 * nrd; 231 | for (var i = 1; i <= nrd; ++i) 232 | dens[krn + i] = ySafe[1][i]; 233 | // compute first derivative at right end 234 | for (var i = 1; i <= n; ++i) 235 | yh1[i] = t[1][i]; 236 | F(x, yh1, yh2); 237 | krn = 3 * nrd; 238 | for (var i = 1; i <= nrd; ++i) 239 | dens[krn + i] = yh2[icom[i]] * h; 240 | // THE LOOP 241 | for (var kmi = 1; kmi <= kmit; ++kmi) { 242 | // compute kmi-th derivative at mid-point 243 | var kbeg = (kmi + 1) / 2 | 0; 244 | for (var kk = kbeg; kk <= kc; ++kk) { 245 | var facnj = Math.pow((nj[kk] / 2), (kmi - 1)); 246 | iPt = iPoint[kk + 1] - 2 * kk + kmi; 247 | for (var i = 1; i <= nrd; ++i) { 248 | ySafe[kk][i] = fSafe[iPt][i] * facnj; 249 | } 250 | } 251 | for (var j = kbeg + 1; j <= kc; ++j) { 252 | var dblenj = nj[j]; 253 | for (var l = j; l >= kbeg + 1; --l) { 254 | var factor = Math.pow((dblenj / nj[l - 1]), 2) - 1; 255 | for (var i = 1; i <= nrd; ++i) { 256 | ySafe[l - 1][i] = ySafe[l][i] + (ySafe[l][i] - ySafe[l - 1][i]) / factor; 257 | } 258 | } 259 | } 260 | krn = (kmi + 4) * nrd; 261 | for (var i = 1; i <= nrd; ++i) 262 | dens[krn + i] = ySafe[kbeg][i] * h; 263 | if (kmi === kmit) 264 | continue; 265 | // compute differences 266 | for (var kk = (kmi + 2) / 2 | 0; kk <= kc; ++kk) { 267 | var lbeg = iPoint[kk + 1]; 268 | var lend = iPoint[kk] + kmi + 1; 269 | if (kmi === 1 && nSeq === 4) 270 | lend += 2; 271 | var l = void 0; 272 | for (l = lbeg; l >= lend; l -= 2) { 273 | for (var i = 1; i <= nrd; ++i) { 274 | fSafe[l][i] -= fSafe[l - 2][i]; 275 | } 276 | } 277 | if (kmi === 1 && nSeq === 4) { 278 | l = lend - 2; 279 | for (var i = 1; i <= nrd; ++i) 280 | fSafe[l][i] -= dz[icom[i]]; 281 | } 282 | } 283 | // compute differences 284 | for (var kk = (kmi + 2) / 2 | 0; kk <= kc; ++kk) { 285 | var lbeg = iPoint[kk + 1] - 1; 286 | var lend = iPoint[kk] + kmi + 2; 287 | for (var l = lbeg; l >= lend; l -= 2) { 288 | for (var i = 1; i <= nrd; ++i) { 289 | fSafe[l][i] -= fSafe[l - 2][i]; 290 | } 291 | } 292 | } 293 | } 294 | interp(nrd, dens, kmit); 295 | // estimation of interpolation error 296 | if (_this.denseOutputErrorEstimator && kmit >= 1) { 297 | var errint = 0; 298 | for (var i = 1; i <= nrd; ++i) 299 | errint += Math.pow((dens[(kmit + 4) * nrd + i] / scal[icom[i]]), 2); 300 | errint = Math.sqrt(errint / nrd) * errfac[kmit]; 301 | hoptde = h / Math.max(Math.pow(errint, (1 / (kmit + 4))), 0.01); 302 | if (errint > 10) { 303 | h = hoptde; 304 | x = xOld; 305 | ++nReject; 306 | reject = true; 307 | return true; 308 | } 309 | } 310 | for (var i = 1; i <= n; ++i) 311 | dz[i] = yh2[i]; 312 | } 313 | for (var i = 1; i <= n; ++i) 314 | y[i] = t[1][i]; 315 | ++nAccept; 316 | if (solOut) { 317 | // If denseOutput, we also want to supply the dense closure. 318 | if (solOut(nAccept + 1, xOld, x, y.slice(1), _this.denseOutput && contex(xOldd, hhh, kmit, dens, icom)) === false) 319 | return false; 320 | } 321 | // compute optimal order 322 | var kopt; 323 | if (kc === 2) { 324 | kopt = Math.min(3, km - 1); 325 | if (reject) 326 | kopt = 2; 327 | } 328 | else { 329 | if (kc <= k) { 330 | kopt = kc; 331 | if (w[kc - 1] < w[kc] * _this.stepSizeFac3) 332 | kopt = kc - 1; 333 | if (w[kc] < w[kc - 1] * _this.stepSizeFac4) 334 | kopt = Math.min(kc + 1, km - 1); 335 | } 336 | else { 337 | kopt = kc - 1; 338 | if (kc > 3 && w[kc - 2] < w[kc - 1] * _this.stepSizeFac3) 339 | kopt = kc - 2; 340 | if (w[kc] < w[kopt] * _this.stepSizeFac4) 341 | kopt = Math.min(kc, km - 1); 342 | } 343 | } 344 | // after a rejected step 345 | if (reject) { 346 | k = Math.min(kopt, kc); 347 | h = posneg * Math.min(Math.abs(h), Math.abs(hh[k])); 348 | reject = false; 349 | return true; // goto 10 350 | } 351 | if (kopt <= kc) { 352 | h = hh[kopt]; 353 | } 354 | else { 355 | if (kc < k && w[kc] < w[kc - 1] * _this.stepSizeFac4) { 356 | h = hh[kc] * a[kopt + 1] / a[kc]; 357 | } 358 | else { 359 | h = hh[kc] * a[kopt] / a[kc]; 360 | } 361 | } 362 | // compute stepsize for next step 363 | k = kopt; 364 | h = posneg * Math.abs(h); 365 | return true; 366 | }; 367 | var midex = function (j) { 368 | var dy = Solver.dim(_this.n); 369 | // Computes the jth line of the extrapolation table and 370 | // provides an estimation of the optional stepsize 371 | var hj = h / nj[j]; 372 | // Euler starting step 373 | for (var i = 1; i <= _this.n; ++i) { 374 | yh1[i] = y[i]; 375 | yh2[i] = y[i] + hj * dz[i]; 376 | } 377 | // Explicit midpoint rule 378 | var m = nj[j] - 1; 379 | var njMid = (nj[j] / 2) | 0; 380 | for (var mm = 1; mm <= m; ++mm) { 381 | if (_this.denseOutput && mm === njMid) { 382 | for (var i = 1; i <= nrd; ++i) { 383 | ySafe[j][i] = yh2[icom[i]]; 384 | } 385 | } 386 | F(x + hj * mm, yh2, dy); 387 | if (_this.denseOutput && Math.abs(mm - njMid) <= 2 * j - 1) { 388 | ++iPt; 389 | for (var i = 1; i <= nrd; ++i) { 390 | fSafe[iPt][i] = dy[icom[i]]; 391 | } 392 | } 393 | for (var i = 1; i <= _this.n; ++i) { 394 | var ys = yh1[i]; 395 | yh1[i] = yh2[i]; 396 | yh2[i] = ys + 2 * hj * dy[i]; 397 | } 398 | if (mm <= _this.stabilityCheckCount && j <= _this.stabilityCheckTableLines) { 399 | // stability check 400 | var del1 = 0; 401 | for (var i = 1; i <= _this.n; ++i) { 402 | del1 += Math.pow((dz[i] / scal[i]), 2); 403 | } 404 | var del2 = 0; 405 | for (var i = 1; i <= _this.n; ++i) { 406 | del2 += Math.pow(((dy[i] - dz[i]) / scal[i]), 2); 407 | } 408 | var quot = del2 / Math.max(_this.uRound, del1); 409 | if (quot > 4) { 410 | ++nEval; 411 | atov = true; 412 | h *= _this.stepSizeReductionFactor; 413 | reject = true; 414 | return; 415 | } 416 | } 417 | } 418 | // final smoothing step 419 | F(x + h, yh2, dy); 420 | if (_this.denseOutput && njMid <= 2 * j - 1) { 421 | ++iPt; 422 | for (var i = 1; i <= nrd; ++i) { 423 | fSafe[iPt][i] = dy[icom[i]]; 424 | } 425 | } 426 | for (var i = 1; i <= _this.n; ++i) { 427 | t[j][i] = (yh1[i] + yh2[i] + hj * dy[i]) / 2; 428 | } 429 | nEval += nj[j]; 430 | // polynomial extrapolation 431 | if (j === 1) 432 | return; // was j.eq.1 433 | var dblenj = nj[j]; 434 | var fac; 435 | for (var l = j; l > 1; --l) { 436 | fac = Math.pow((dblenj / nj[l - 1]), 2) - 1; 437 | for (var i = 1; i <= _this.n; ++i) { 438 | t[l - 1][i] = t[l][i] + (t[l][i] - t[l - 1][i]) / fac; 439 | } 440 | } 441 | err = 0; 442 | // scaling 443 | for (var i = 1; i <= _this.n; ++i) { 444 | var t1i = Math.max(Math.abs(y[i]), Math.abs(t[1][i])); 445 | scal[i] = aTol[i] + rTol[i] * t1i; 446 | err += Math.pow(((t[1][i] - t[2][i]) / scal[i]), 2); 447 | } 448 | err = Math.sqrt(err / _this.n); 449 | if (err * _this.uRound >= 1 || (j > 2 && err >= errOld)) { 450 | atov = true; 451 | h *= _this.stepSizeReductionFactor; 452 | reject = true; 453 | return; 454 | } 455 | errOld = Math.max(4 * err, 1); 456 | // compute optimal stepsizes 457 | var exp0 = 1 / (2 * j - 1); 458 | var facMin = Math.pow(_this.stepSizeFac1, exp0); 459 | fac = Math.min(_this.stepSizeFac2 / facMin, Math.max(facMin, Math.pow((err / _this.stepSafetyFactor1), exp0) / _this.stepSafetyFactor2)); 460 | fac = 1 / fac; 461 | hh[j] = Math.min(Math.abs(h) * fac, hMax); 462 | w[j] = a[j] / hh[j]; 463 | }; 464 | var interp = function (n, y, imit) { 465 | // computes the coefficients of the interpolation formula 466 | var a = new Array(31); // zero-based: 0:30 467 | // begin with Hermite interpolation 468 | for (var i = 1; i <= n; ++i) { 469 | var y0_1 = y[i]; 470 | var y1 = y[2 * n + i]; 471 | var yp0 = y[n + i]; 472 | var yp1 = y[3 * n + i]; 473 | var yDiff = y1 - y0_1; 474 | var aspl = -yp1 + yDiff; 475 | var bspl = yp0 - yDiff; 476 | y[n + i] = yDiff; 477 | y[2 * n + i] = aspl; 478 | y[3 * n + i] = bspl; 479 | if (imit < 0) 480 | continue; 481 | // compute the derivatives of Hermite at midpoint 482 | var ph0 = (y0_1 + y1) * 0.5 + 0.125 * (aspl + bspl); 483 | var ph1 = yDiff + (aspl - bspl) * 0.25; 484 | var ph2 = -(yp0 - yp1); 485 | var ph3 = 6 * (bspl - aspl); 486 | // compute the further coefficients 487 | if (imit >= 1) { 488 | a[1] = 16 * (y[5 * n + i] - ph1); 489 | if (imit >= 3) { 490 | a[3] = 16 * (y[7 * n + i] - ph3 + 3 * a[1]); 491 | if (imit >= 5) { 492 | for (var im = 5; im <= imit; im += 2) { 493 | var fac1 = im * (im - 1) / 2; 494 | var fac2 = fac1 * (im - 2) * (im - 3) * 2; 495 | a[im] = 16 * (y[(im + 4) * n + i] + fac1 * a[im - 2] - fac2 * a[im - 4]); 496 | } 497 | } 498 | } 499 | } 500 | a[0] = (y[4 * n + i] - ph0) * 16; 501 | if (imit >= 2) { 502 | a[2] = (y[n * 6 + i] - ph2 + a[0]) * 16; 503 | if (imit >= 4) { 504 | for (var im = 4; im <= imit; im += 2) { 505 | var fac1 = im * (im - 1) / 2; 506 | var fac2 = im * (im - 1) * (im - 2) * (im - 3); 507 | a[im] = (y[n * (im + 4) + i] + a[im - 2] * fac1 - a[im - 4] * fac2) * 16; 508 | } 509 | } 510 | } 511 | for (var im = 0; im <= imit; ++im) 512 | y[n * (im + 4) + i] = a[im]; 513 | } 514 | }; 515 | var contex = function (xOld, h, imit, y, icom) { 516 | return function (c, x) { 517 | var i = 0; 518 | for (var j = 1; j <= nrd; ++j) { 519 | // careful: customers describe components 0-based. We record indices 1-based. 520 | if (icom[j] === c + 1) 521 | i = j; 522 | } 523 | if (i === 0) 524 | throw new Error('no dense output available for component ' + c); 525 | var theta = (x - xOld) / h; 526 | var theta1 = 1 - theta; 527 | var phthet = y[i] + theta * (y[nrd + i] + theta1 * (y[2 * nrd + i] * theta + y[3 * nrd + i] * theta1)); 528 | if (imit < 0) 529 | return phthet; 530 | var thetah = theta - 0.5; 531 | var ret = y[nrd * (imit + 4) + i]; 532 | for (var im = imit; im >= 1; --im) { 533 | ret = y[nrd * (im + 3) + i] + ret * thetah / im; 534 | } 535 | return phthet + Math.pow((theta * theta1), 2) * ret; 536 | }; 537 | }; 538 | // preparation 539 | var ySafe = Solver.dim2(km, nrd); 540 | var hh = Solver.dim(km); 541 | var t = Solver.dim2(km, _this.n); 542 | // Define the step size sequence 543 | var nj = Solver.stepSizeSequence(nSeq, km); 544 | // Define the a[i] for order selection 545 | var a = Solver.dim(km); 546 | a[1] = 1 + nj[1]; 547 | for (var i = 2; i <= km; ++i) { 548 | a[i] = a[i - 1] + nj[i]; 549 | } 550 | // Initial Scaling 551 | var scal = Solver.dim(_this.n); 552 | for (var i = 1; i <= _this.n; ++i) { 553 | scal[i] = aTol[i] + rTol[i] + Math.abs(y[i]); 554 | } 555 | // Initial preparations 556 | var posneg = xEnd - x >= 0 ? 1 : -1; 557 | var k = Math.max(2, Math.min(km - 1, Math.floor(-Solver.log10(rTol[1] + 1e-40) * 0.6 + 1.5))); 558 | var h = Math.max(Math.abs(_this.initialStepSize), 1e-4); 559 | h = posneg * Math.min(h, hMax, Math.abs(xEnd - x) / 2); 560 | var iPoint = Solver.dim(km + 1); 561 | var errfac = Solver.dim(2 * km); 562 | var xOld = x; 563 | var iPt = 0; 564 | if (solOut) { 565 | if (_this.denseOutput) { 566 | iPoint[1] = 0; 567 | for (var i = 1; i <= km; ++i) { 568 | var njAdd = 4 * i - 2; 569 | if (nj[i] > njAdd) 570 | ++njAdd; 571 | iPoint[i + 1] = iPoint[i] + njAdd; 572 | } 573 | for (var mu = 1; mu <= 2 * km; ++mu) { 574 | var errx = Math.sqrt(mu / (mu + 4)) * 0.5; 575 | var prod = Math.pow((1 / (mu + 4)), 2); 576 | for (var j = 1; j <= mu; ++j) 577 | prod *= errx / j; 578 | errfac[mu] = prod; 579 | } 580 | iPt = 0; 581 | } 582 | // check return value and abandon integration if called for 583 | if (false === solOut(nAccept + 1, xOld, x, y.slice(1))) { 584 | return Outcome.EarlyReturn; 585 | } 586 | } 587 | var err = 0; 588 | var errOld = 1e10; 589 | var hoptde = posneg * hMax; 590 | var w = Solver.dim(km); 591 | w[1] = 0; 592 | var reject = false; 593 | var last = false; 594 | var atov; 595 | var kc = 0; 596 | var STATE; 597 | (function (STATE) { 598 | STATE[STATE["Start"] = 0] = "Start"; 599 | STATE[STATE["BasicIntegrationStep"] = 1] = "BasicIntegrationStep"; 600 | STATE[STATE["ConvergenceStep"] = 2] = "ConvergenceStep"; 601 | STATE[STATE["HopeForConvergence"] = 3] = "HopeForConvergence"; 602 | STATE[STATE["Accept"] = 4] = "Accept"; 603 | STATE[STATE["Reject"] = 5] = "Reject"; 604 | })(STATE || (STATE = {})); 605 | var state = STATE.Start; 606 | loop: while (true) { 607 | _this.debug && console.log('STATE', STATE[state], nStep, xOld, x, h, k, kc, hoptde); 608 | switch (state) { 609 | case STATE.Start: 610 | atov = false; 611 | // Is xEnd reached in the next step? 612 | if (0.1 * Math.abs(xEnd - x) <= Math.abs(x) * _this.uRound) 613 | break loop; 614 | h = posneg * Math.min(Math.abs(h), Math.abs(xEnd - x), hMax, Math.abs(hoptde)); 615 | if ((x + 1.01 * h - xEnd) * posneg > 0) { 616 | h = xEnd - x; 617 | last = true; 618 | } 619 | if (nStep === 0 || !_this.denseOutput) { 620 | F(x, y, dz); 621 | ++nEval; 622 | } 623 | // The first and last step 624 | if (nStep === 0 || last) { 625 | iPt = 0; 626 | ++nStep; 627 | for (var j = 1; j <= k; ++j) { 628 | kc = j; 629 | midex(j); 630 | if (atov) 631 | continue loop; 632 | if (j > 1 && err <= 1) { 633 | state = STATE.Accept; 634 | continue loop; 635 | } 636 | } 637 | state = STATE.HopeForConvergence; 638 | continue; 639 | } 640 | state = STATE.BasicIntegrationStep; 641 | continue; 642 | case STATE.BasicIntegrationStep: 643 | // basic integration step 644 | iPt = 0; 645 | ++nStep; 646 | if (nStep >= _this.maxSteps) { 647 | return Outcome.MaxStepsExceeded; 648 | } 649 | kc = k - 1; 650 | for (var j = 1; j <= kc; ++j) { 651 | midex(j); 652 | if (atov) { 653 | state = STATE.Start; 654 | continue loop; 655 | } 656 | } 657 | // convergence monitor 658 | if (k === 2 || reject) { 659 | state = STATE.ConvergenceStep; 660 | } 661 | else { 662 | if (err <= 1) { 663 | state = STATE.Accept; 664 | } 665 | else if (err > Math.pow(((nj[k + 1] * nj[k]) / 4), 2)) { 666 | state = STATE.Reject; 667 | } 668 | else 669 | state = STATE.ConvergenceStep; 670 | } 671 | continue; 672 | case STATE.ConvergenceStep: 673 | midex(k); 674 | if (atov) { 675 | state = STATE.Start; 676 | continue; 677 | } 678 | kc = k; 679 | if (err <= 1) { 680 | state = STATE.Accept; 681 | continue; 682 | } 683 | state = STATE.HopeForConvergence; 684 | continue; 685 | case STATE.HopeForConvergence: 686 | // hope for convergence in line k + 1 687 | if (err > Math.pow((nj[k + 1] / 2), 2)) { 688 | state = STATE.Reject; 689 | continue; 690 | } 691 | kc = k + 1; 692 | midex(kc); 693 | if (atov) 694 | state = STATE.Start; 695 | else if (err > 1) 696 | state = STATE.Reject; 697 | else 698 | state = STATE.Accept; 699 | continue; 700 | case STATE.Accept: 701 | if (!acceptStep(_this.n)) 702 | return Outcome.EarlyReturn; 703 | state = STATE.Start; 704 | continue; 705 | case STATE.Reject: 706 | k = Math.min(k, kc, km - 1); 707 | if (k > 2 && w[k - 1] < w[k] * _this.stepSizeFac3) 708 | k -= 1; 709 | ++nReject; 710 | h = posneg * hh[k]; 711 | reject = true; 712 | state = STATE.BasicIntegrationStep; 713 | } 714 | } 715 | return Outcome.Converged; 716 | }; 717 | var outcome = odxcor(); 718 | return { 719 | y: y.slice(1), 720 | outcome: outcome, 721 | nStep: nStep, 722 | xEnd: xEnd, 723 | nAccept: nAccept, 724 | nReject: nReject, 725 | nEval: nEval 726 | }; 727 | }; 728 | return Solver; 729 | }()); 730 | // return a 1-based array of length n. Initial values undefined. 731 | Solver.dim = function (n) { return Array(n + 1); }; 732 | Solver.log10 = function (x) { return Math.log(x) / Math.LN10; }; 733 | exports.Solver = Solver; 734 | //# sourceMappingURL=odex.js.map -------------------------------------------------------------------------------- /src/odex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An implementation of ODEX, by E. Hairer and G. Wanner, ported from the Fortran ODEX.F. 3 | * The original work carries the BSD 2-clause license, and so does this. 4 | * 5 | * Copyright (c) 2016 Colin Smith. 6 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | * disclaimer. 8 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 9 | * following disclaimer in the documentation and/or other materials provided with the distribution. 10 | * 11 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 12 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 13 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 15 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 16 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 17 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 | */ 19 | 20 | export interface Derivative { // function computing the value of Y' = F(x,Y) 21 | (x: number, // input x value 22 | y: number[]) // input y value) 23 | : number[] // output y' values (Array of length n) 24 | } 25 | 26 | export interface OutputFunction { // value callback 27 | (nr: number, // step number 28 | xold: number, // left edge of solution interval 29 | x: number, // right edge of solution interval (y = F(x)) 30 | y: number[], // F(x) 31 | dense?: (c: number, x: number) => number) // dense interpolator. Valid in the range [x, xold). 32 | : boolean|void // return false to halt integration 33 | } 34 | 35 | export enum Outcome { 36 | Converged, 37 | MaxStepsExceeded, 38 | EarlyReturn 39 | } 40 | 41 | export class Solver { 42 | n: number // dimension of the system 43 | uRound: number // WORK(1), machine epsilon. (WORK, IWORK are references to odex.f) 44 | maxSteps: number // IWORK(1), positive integer 45 | initialStepSize: number // H 46 | maxStepSize: number // WORK(2), maximal step size, default xEnd - x 47 | maxExtrapolationColumns: number // IWORK(2), KM, positive integer 48 | stepSizeSequence: number // IWORK(3), in [1..5] 49 | stabilityCheckCount: number // IWORK(4), in 50 | stabilityCheckTableLines: number // IWORK(5), positive integer 51 | denseOutput: boolean // IOUT >= 2, true means dense output interpolator provided to solOut 52 | denseOutputErrorEstimator: boolean // IWORK(6), reversed sense from the FORTRAN code 53 | denseComponents: number[] // IWORK(8) & IWORK(21,...), components for which dense output is required 54 | interpolationFormulaDegree: number // IWORK(7), µ = 2 * k - interpolationFormulaDegree + 1 [1..6], default 4 55 | stepSizeReductionFactor: number // WORK(3), default 0.5 56 | stepSizeFac1: number // WORK(4) 57 | stepSizeFac2: number // WORK(5) 58 | stepSizeFac3: number // WORK(6) 59 | stepSizeFac4: number // WORK(7) 60 | stepSafetyFactor1: number // WORK(8) 61 | stepSafetyFactor2: number // WORK(9) 62 | relativeTolerance: number|number[] // RTOL. Can be a scalar or vector of length N. 63 | absoluteTolerance: number|number[] // ATOL. Can be a scalar or vector of length N. 64 | debug: boolean 65 | 66 | constructor(n: number) { 67 | this.n = n 68 | this.uRound = 2.3e-16 69 | this.maxSteps = 10000 70 | this.initialStepSize = 1e-4 71 | this.maxStepSize = 0 72 | this.maxExtrapolationColumns = 9 73 | this.stepSizeSequence = 0 74 | this.stabilityCheckCount = 1 75 | this.stabilityCheckTableLines = 2 76 | this.denseOutput = false 77 | this.denseOutputErrorEstimator = true 78 | this.denseComponents = undefined 79 | this.interpolationFormulaDegree = 4 80 | this.stepSizeReductionFactor = 0.5 81 | this.stepSizeFac1 = 0.02 82 | this.stepSizeFac2 = 4.0 83 | this.stepSizeFac3 = 0.8 84 | this.stepSizeFac4 = 0.9 85 | this.stepSafetyFactor1 = 0.65 86 | this.stepSafetyFactor2 = 0.94 87 | this.relativeTolerance = 1e-5 88 | this.absoluteTolerance = 1e-5 89 | this.debug = false 90 | } 91 | 92 | grid(dt: number, out: (xOut: number, yOut: number[]) => any): OutputFunction { 93 | if (!this.denseOutput) throw new Error('Must set .denseOutput to true when using grid') 94 | let components: number[] = this.denseComponents 95 | if (!components) { 96 | components = [] 97 | for (let i = 0; i < this.n; ++i) components.push(i) 98 | } 99 | let t: number 100 | return (n: number, xOld: number, x: number, y: number[], interpolate: (i: number, x: number) => number) => { 101 | if (n === 1) { 102 | let v = out(x, y) 103 | t = x + dt 104 | return v 105 | } 106 | while (t <= x) { 107 | let yf: number[] = [] 108 | for (let i of components) { 109 | yf.push(interpolate(i, t)) 110 | } 111 | let v = out(t, yf) 112 | if (v === false) return false 113 | t += dt 114 | } 115 | } 116 | } 117 | 118 | // return a 1-based array of length n. Initial values undefined. 119 | private static dim = (n: number) => Array(n + 1) 120 | private static log10 = (x: number) => Math.log(x) / Math.LN10 121 | 122 | // Make a 1-based 2D array, with r rows and c columns. The initial values are undefined. 123 | private static dim2(r: number, c: number): number[][] { 124 | let a = new Array(r + 1) 125 | for (let i = 1; i <= r; ++i) a[i] = Solver.dim(c) 126 | return a 127 | } 128 | 129 | // Generate step size sequence and return as a 1-based array of length n. 130 | static stepSizeSequence(nSeq: number, n: number): number[] { 131 | const a = new Array(n + 1) 132 | a[0] = 0 133 | switch (nSeq) { 134 | case 1: 135 | for (let i = 1; i <= n; ++i) a[i] = 2 * i 136 | break 137 | case 2: 138 | a[1] = 2 139 | for (let i = 2; i <= n; ++i) a[i] = 4 * i - 4 140 | break 141 | case 3: 142 | a[1] = 2 143 | a[2] = 4 144 | a[3] = 6 145 | for (let i = 4; i <= n; ++i) a[i] = 2 * a[i - 2] 146 | break 147 | case 4: 148 | for (let i = 1; i <= n; ++i) a[i] = 4 * i - 2 149 | break 150 | case 5: 151 | for (let i = 1; i <= n; ++i) a[i] = 4 * i 152 | break 153 | default: 154 | throw new Error('invalid stepSizeSequence selected') 155 | } 156 | return a 157 | } 158 | 159 | // Integrate the differential system represented by f, from x to xEnd, with initial data y. 160 | // solOut, if provided, is called at each integration step. 161 | solve(f: Derivative, 162 | x: number, 163 | y0: number[], 164 | xEnd: number, 165 | solOut?: OutputFunction) { 166 | 167 | // Make a copy of y0, 1-based. We leave the user's parameters alone so that they may be reused if desired. 168 | let y = [0].concat(y0) 169 | let dz = Solver.dim(this.n) 170 | let yh1 = Solver.dim(this.n) 171 | let yh2 = Solver.dim(this.n) 172 | if (this.maxSteps <= 0) throw new Error('maxSteps must be positive') 173 | const km = this.maxExtrapolationColumns 174 | if (km <= 2) throw new Error('maxExtrapolationColumns must be > 2') 175 | const nSeq = this.stepSizeSequence || (this.denseOutput ? 4 : 1) 176 | if (nSeq <= 3 && this.denseOutput) throw new Error('stepSizeSequence incompatible with denseOutput') 177 | if (this.denseOutput && !solOut) throw new Error('denseOutput requires a solution observer function') 178 | if (this.interpolationFormulaDegree <= 0 || this.interpolationFormulaDegree >= 7) throw new Error('bad interpolationFormulaDegree') 179 | let icom = [0] // icom will be 1-based, so start with a pad entry. 180 | let nrdens = 0 181 | if (this.denseOutput) { 182 | if (this.denseComponents) { 183 | for (let c of this.denseComponents) { 184 | // convert dense components requested into one-based indexing. 185 | if (c < 0 || c > this.n) throw new Error('bad dense component: ' + c) 186 | icom.push(c + 1) 187 | ++nrdens 188 | } 189 | } else { 190 | // if user asked for dense output but did not specify any denseComponents, 191 | // request all of them. 192 | for (let i = 1; i <= this.n; ++i) { 193 | icom.push(i) 194 | } 195 | nrdens = this.n 196 | } 197 | } 198 | if (this.uRound <= 1e-35 || this.uRound > 1) throw new Error('suspicious value of uRound') 199 | const hMax = Math.abs(this.maxStepSize || xEnd - x) 200 | const lfSafe = 2 * km * km + km 201 | 202 | function expandToArray(x: number|number[], n: number): number[] { 203 | // If x is an array, return a 1-based copy of it. If x is a number, return a new 1-based array 204 | // consisting of n copies of the number. 205 | const tolArray = [0] 206 | if (Array.isArray(x)) { 207 | return tolArray.concat(x) 208 | } else { 209 | for (let i = 0; i < n; ++i) tolArray.push(x) 210 | return tolArray 211 | } 212 | } 213 | 214 | const aTol = expandToArray(this.absoluteTolerance, this.n) 215 | const rTol = expandToArray(this.relativeTolerance, this.n) 216 | let [nEval, nStep, nAccept, nReject] = [0, 0, 0, 0] 217 | 218 | // call to core integrator 219 | const nrd = Math.max(1, nrdens) 220 | const ncom = Math.max(1, (2 * km + 5) * nrdens) 221 | const dens = Solver.dim(ncom) 222 | const fSafe = Solver.dim2(lfSafe, nrd) 223 | 224 | // Wrap f in a function F which hides the one-based indexing from the customers. 225 | const F = (x: number, y: number[], yp: number[]) => { 226 | let ret = f(x, y.slice(1)) 227 | for (let i = 0; i < ret.length; ++i) yp[i + 1] = ret[i] 228 | } 229 | 230 | let odxcor = (): Outcome => { 231 | // The following three variables are COMMON/CONTEX/ 232 | let xOldd: number 233 | let hhh: number 234 | let kmit: number 235 | 236 | let acceptStep = (n: number): boolean => { // label 60 237 | // Returns true if we should continue the integration. The only time false 238 | // is returned is when the user's solution observation function has returned false, 239 | // indicating that she does not wish to continue the computation. 240 | xOld = x 241 | x += h 242 | if (this.denseOutput) { 243 | // kmit = mu of the paper 244 | kmit = 2 * kc - this.interpolationFormulaDegree + 1 245 | for (let i = 1; i <= nrd; ++i) dens[i] = y[icom[i]] 246 | xOldd = xOld 247 | hhh = h // note: xOldd and hhh are part of /CONODX/ 248 | for (let i = 1; i <= nrd; ++i) dens[nrd + i] = h * dz[icom[i]] 249 | let kln = 2 * nrd 250 | for (let i = 1; i <= nrd; ++i) dens[kln + i] = t[1][icom[i]] 251 | // compute solution at mid-point 252 | for (let j = 2; j <= kc; ++j) { 253 | let dblenj = nj[j] 254 | for (let l = j; l >= 2; --l) { 255 | let factor = (dblenj / nj[l - 1]) ** 2 - 1 256 | for (let i = 1; i <= nrd; ++i) { 257 | ySafe[l - 1][i] = ySafe[l][i] + (ySafe[l][i] - ySafe[l - 1][i]) / factor 258 | } 259 | } 260 | } 261 | let krn = 4 * nrd 262 | for (let i = 1; i <= nrd; ++i) dens[krn + i] = ySafe[1][i] 263 | // compute first derivative at right end 264 | for (let i = 1; i <= n; ++i) yh1[i] = t[1][i] 265 | F(x, yh1, yh2) 266 | krn = 3 * nrd 267 | for (let i = 1; i <= nrd; ++i) dens[krn + i] = yh2[icom[i]] * h 268 | // THE LOOP 269 | for (let kmi = 1; kmi <= kmit; ++kmi) { 270 | // compute kmi-th derivative at mid-point 271 | let kbeg = (kmi + 1) / 2 | 0 272 | for (let kk = kbeg; kk <= kc; ++kk) { 273 | let facnj = (nj[kk] / 2) ** (kmi - 1) 274 | iPt = iPoint[kk + 1] - 2 * kk + kmi 275 | for (let i = 1; i <= nrd; ++i) { 276 | ySafe[kk][i] = fSafe[iPt][i] * facnj 277 | } 278 | } 279 | for (let j = kbeg + 1; j <= kc; ++j) { 280 | let dblenj = nj[j] 281 | for (let l = j; l >= kbeg + 1; --l) { 282 | let factor = (dblenj / nj[l - 1]) ** 2 - 1 283 | for (let i = 1; i <= nrd; ++i) { 284 | ySafe[l - 1][i] = ySafe[l][i] + (ySafe[l][i] - ySafe[l - 1][i]) / factor 285 | } 286 | } 287 | } 288 | krn = (kmi + 4) * nrd 289 | for (let i = 1; i <= nrd; ++i) dens[krn + i] = ySafe[kbeg][i] * h 290 | if (kmi === kmit) continue 291 | // compute differences 292 | for (let kk = (kmi + 2) / 2 | 0; kk <= kc; ++kk) { 293 | let lbeg = iPoint[kk + 1] 294 | let lend = iPoint[kk] + kmi + 1 295 | if (kmi === 1 && nSeq === 4) lend += 2 296 | let l: number 297 | for (l = lbeg; l >= lend; l -= 2) { 298 | for (let i = 1; i <= nrd; ++i) { 299 | fSafe[l][i] -= fSafe[l - 2][i] 300 | } 301 | } 302 | if (kmi === 1 && nSeq === 4) { 303 | l = lend - 2 304 | for (let i = 1; i <= nrd; ++i) fSafe[l][i] -= dz[icom[i]] 305 | } 306 | } 307 | // compute differences 308 | for (let kk = (kmi + 2) / 2 | 0; kk <= kc; ++kk) { 309 | let lbeg = iPoint[kk + 1] - 1 310 | let lend = iPoint[kk] + kmi + 2 311 | for (let l = lbeg; l >= lend; l -= 2) { 312 | for (let i = 1; i <= nrd; ++i) { 313 | fSafe[l][i] -= fSafe[l - 2][i] 314 | } 315 | } 316 | } 317 | } 318 | interp(nrd, dens, kmit) 319 | // estimation of interpolation error 320 | if (this.denseOutputErrorEstimator && kmit >= 1) { 321 | let errint = 0 322 | for (let i = 1; i <= nrd; ++i) errint += (dens[(kmit + 4) * nrd + i] / scal[icom[i]]) ** 2 323 | errint = Math.sqrt(errint / nrd) * errfac[kmit] 324 | hoptde = h / Math.max(errint ** (1 / (kmit + 4)), 0.01) 325 | if (errint > 10) { 326 | h = hoptde 327 | x = xOld 328 | ++nReject 329 | reject = true 330 | return true 331 | } 332 | } 333 | for (let i = 1; i <= n; ++i) dz[i] = yh2[i] 334 | } 335 | for (let i = 1; i <= n; ++i) y[i] = t[1][i] 336 | ++nAccept 337 | if (solOut) { 338 | // If denseOutput, we also want to supply the dense closure. 339 | if (solOut(nAccept + 1, xOld, x, y.slice(1), 340 | this.denseOutput && contex(xOldd, hhh, kmit, dens, icom)) === false) return false 341 | } 342 | // compute optimal order 343 | let kopt: number 344 | if (kc === 2) { 345 | kopt = Math.min(3, km - 1) 346 | if (reject) kopt = 2 347 | } else { 348 | if (kc <= k) { 349 | kopt = kc 350 | if (w[kc - 1] < w[kc] * this.stepSizeFac3) kopt = kc - 1 351 | if (w[kc] < w[kc - 1] * this.stepSizeFac4) kopt = Math.min(kc + 1, km - 1) 352 | } else { 353 | kopt = kc - 1 354 | if (kc > 3 && w[kc - 2] < w[kc - 1] * this.stepSizeFac3) kopt = kc - 2 355 | if (w[kc] < w[kopt] * this.stepSizeFac4) kopt = Math.min(kc, km - 1) 356 | } 357 | } 358 | // after a rejected step 359 | if (reject) { 360 | k = Math.min(kopt, kc) 361 | h = posneg * Math.min(Math.abs(h), Math.abs(hh[k])) 362 | reject = false 363 | return true // goto 10 364 | } 365 | if (kopt <= kc) { 366 | h = hh[kopt] 367 | } else { 368 | if (kc < k && w[kc] < w[kc - 1] * this.stepSizeFac4) { 369 | h = hh[kc] * a[kopt + 1] / a[kc] 370 | } else { 371 | h = hh[kc] * a[kopt] / a[kc] 372 | } 373 | 374 | 375 | } 376 | // compute stepsize for next step 377 | k = kopt 378 | h = posneg * Math.abs(h) 379 | return true 380 | } 381 | 382 | let midex = (j: number): void => { 383 | const dy = Solver.dim(this.n) 384 | // Computes the jth line of the extrapolation table and 385 | // provides an estimation of the optional stepsize 386 | const hj = h / nj[j] 387 | // Euler starting step 388 | for (let i = 1; i <= this.n; ++i) { 389 | yh1[i] = y[i] 390 | yh2[i] = y[i] + hj * dz[i] 391 | } 392 | // Explicit midpoint rule 393 | const m = nj[j] - 1 394 | const njMid = (nj[j] / 2) | 0 395 | for (let mm = 1; mm <= m; ++mm) { 396 | if (this.denseOutput && mm === njMid) { 397 | for (let i = 1; i <= nrd; ++i) { 398 | ySafe[j][i] = yh2[icom[i]] 399 | } 400 | } 401 | F(x + hj * mm, yh2, dy) 402 | if (this.denseOutput && Math.abs(mm - njMid) <= 2 * j - 1) { 403 | ++iPt 404 | for (let i = 1; i <= nrd; ++i) { 405 | fSafe[iPt][i] = dy[icom[i]] 406 | } 407 | } 408 | for (let i = 1; i <= this.n; ++i) { 409 | let ys = yh1[i] 410 | yh1[i] = yh2[i] 411 | yh2[i] = ys + 2 * hj * dy[i] 412 | } 413 | if (mm <= this.stabilityCheckCount && j <= this.stabilityCheckTableLines) { 414 | // stability check 415 | let del1 = 0 416 | for (let i = 1; i <= this.n; ++i) { 417 | del1 += (dz[i] / scal[i]) ** 2 418 | } 419 | let del2 = 0 420 | for (let i = 1; i <= this.n; ++i) { 421 | del2 += ((dy[i] - dz[i]) / scal[i]) ** 2 422 | } 423 | const quot = del2 / Math.max(this.uRound, del1) 424 | if (quot > 4) { 425 | ++nEval 426 | atov = true 427 | h *= this.stepSizeReductionFactor 428 | reject = true 429 | return 430 | } 431 | } 432 | } 433 | // final smoothing step 434 | F(x + h, yh2, dy) 435 | if (this.denseOutput && njMid <= 2 * j - 1) { 436 | ++iPt 437 | for (let i = 1; i <= nrd; ++i) { 438 | fSafe[iPt][i] = dy[icom[i]] 439 | } 440 | } 441 | for (let i = 1; i <= this.n; ++i) { 442 | t[j][i] = (yh1[i] + yh2[i] + hj * dy[i]) / 2 443 | } 444 | nEval += nj[j] 445 | // polynomial extrapolation 446 | if (j === 1) return // was j.eq.1 447 | const dblenj = nj[j] 448 | let fac: number 449 | for (let l = j; l > 1; --l) { 450 | fac = (dblenj / nj[l - 1]) ** 2 - 1 451 | for (let i = 1; i <= this.n; ++i) { 452 | t[l - 1][i] = t[l][i] + (t[l][i] - t[l - 1][i]) / fac 453 | } 454 | } 455 | err = 0 456 | // scaling 457 | for (let i = 1; i <= this.n; ++i) { 458 | let t1i = Math.max(Math.abs(y[i]), Math.abs(t[1][i])) 459 | scal[i] = aTol[i] + rTol[i] * t1i 460 | err += ((t[1][i] - t[2][i]) / scal[i]) ** 2 461 | } 462 | err = Math.sqrt(err / this.n) 463 | if (err * this.uRound >= 1 || (j > 2 && err >= errOld)) { 464 | atov = true 465 | h *= this.stepSizeReductionFactor 466 | reject = true 467 | return 468 | } 469 | errOld = Math.max(4 * err, 1) 470 | // compute optimal stepsizes 471 | let exp0 = 1 / (2 * j - 1) 472 | let facMin = this.stepSizeFac1 ** exp0 473 | fac = Math.min(this.stepSizeFac2 / facMin, 474 | Math.max(facMin, (err / this.stepSafetyFactor1) ** exp0 / this.stepSafetyFactor2)) 475 | fac = 1 / fac 476 | hh[j] = Math.min(Math.abs(h) * fac, hMax) 477 | w[j] = a[j] / hh[j] 478 | } 479 | 480 | const interp = (n: number, y: number[], imit: number) => { 481 | // computes the coefficients of the interpolation formula 482 | let a = new Array(31) // zero-based: 0:30 483 | // begin with Hermite interpolation 484 | for (let i = 1; i <= n; ++i) { 485 | let y0 = y[i] 486 | let y1 = y[2 * n + i] 487 | let yp0 = y[n + i] 488 | let yp1 = y[3 * n + i] 489 | let yDiff = y1 - y0 490 | let aspl = -yp1 + yDiff 491 | let bspl = yp0 - yDiff 492 | y[n + i] = yDiff 493 | y[2 * n + i] = aspl 494 | y[3 * n + i] = bspl 495 | if (imit < 0) continue 496 | // compute the derivatives of Hermite at midpoint 497 | let ph0 = (y0 + y1) * 0.5 + 0.125 * (aspl + bspl) 498 | let ph1 = yDiff + (aspl - bspl) * 0.25 499 | let ph2 = -(yp0 - yp1) 500 | let ph3 = 6 * (bspl - aspl) 501 | // compute the further coefficients 502 | if (imit >= 1) { 503 | a[1] = 16 * (y[5 * n + i] - ph1) 504 | if (imit >= 3) { 505 | a[3] = 16 * (y[7 * n + i] - ph3 + 3 * a[1]) 506 | if (imit >= 5) { 507 | for (let im = 5; im <= imit; im += 2) { 508 | let fac1 = im * (im - 1) / 2 509 | let fac2 = fac1 * (im - 2) * (im - 3) * 2 510 | a[im] = 16 * (y[(im + 4) * n + i] + fac1 * a[im - 2] - fac2 * a[im - 4]) 511 | } 512 | } 513 | } 514 | } 515 | a[0] = (y[4 * n + i] - ph0) * 16 516 | if (imit >= 2) { 517 | a[2] = (y[n * 6 + i] - ph2 + a[0]) * 16 518 | if (imit >= 4) { 519 | for (let im = 4; im <= imit; im += 2) { 520 | let fac1 = im * (im - 1) / 2 521 | let fac2 = im * (im - 1) * (im - 2) * (im - 3) 522 | a[im] = (y[n * (im + 4) + i] + a[im - 2] * fac1 - a[im - 4] * fac2) * 16 523 | } 524 | } 525 | } 526 | for (let im = 0; im <= imit; ++im) y[n * (im + 4) + i] = a[im] 527 | } 528 | } 529 | 530 | const contex = (xOld: number, 531 | h: number, 532 | imit: number, 533 | y: number[], 534 | icom: number[]) => { 535 | return (c: number, x: number) => { 536 | let i = 0 537 | for (let j = 1; j <= nrd; ++j) { 538 | // careful: customers describe components 0-based. We record indices 1-based. 539 | if (icom[j] === c + 1) i = j 540 | } 541 | if (i === 0) throw new Error('no dense output available for component ' + c) 542 | const theta = (x - xOld) / h 543 | const theta1 = 1 - theta 544 | const phthet = y[i] + theta * (y[nrd + i] + theta1 * (y[2 * nrd + i] * theta + y[3 * nrd + i] * theta1)) 545 | if (imit < 0) return phthet 546 | const thetah = theta - 0.5 547 | let ret = y[nrd * (imit + 4) + i] 548 | for (let im = imit; im >= 1; --im) { 549 | ret = y[nrd * (im + 3) + i] + ret * thetah / im 550 | } 551 | return phthet + (theta * theta1) ** 2 * ret 552 | } 553 | } 554 | 555 | // preparation 556 | const ySafe = Solver.dim2(km, nrd) 557 | const hh = Solver.dim(km) 558 | const t = Solver.dim2(km, this.n) 559 | // Define the step size sequence 560 | const nj = Solver.stepSizeSequence(nSeq, km) 561 | // Define the a[i] for order selection 562 | const a = Solver.dim(km) 563 | a[1] = 1 + nj[1] 564 | for (let i = 2; i <= km; ++i) { 565 | a[i] = a[i - 1] + nj[i] 566 | } 567 | // Initial Scaling 568 | const scal = Solver.dim(this.n) 569 | for (let i = 1; i <= this.n; ++i) { 570 | scal[i] = aTol[i] + rTol[i] + Math.abs(y[i]) 571 | } 572 | // Initial preparations 573 | const posneg = xEnd - x >= 0 ? 1 : -1 574 | let k = Math.max(2, Math.min(km - 1, Math.floor(-Solver.log10(rTol[1] + 1e-40) * 0.6 + 1.5))) 575 | let h = Math.max(Math.abs(this.initialStepSize), 1e-4) 576 | h = posneg * Math.min(h, hMax, Math.abs(xEnd - x) / 2) 577 | const iPoint = Solver.dim(km + 1) 578 | const errfac = Solver.dim(2 * km) 579 | let xOld = x 580 | let iPt = 0 581 | if (solOut) { 582 | if (this.denseOutput) { 583 | iPoint[1] = 0 584 | for (let i = 1; i <= km; ++i) { 585 | let njAdd = 4 * i - 2 586 | if (nj[i] > njAdd) ++njAdd 587 | iPoint[i + 1] = iPoint[i] + njAdd 588 | } 589 | for (let mu = 1; mu <= 2 * km; ++mu) { 590 | let errx = Math.sqrt(mu / (mu + 4)) * 0.5 591 | let prod = (1 / (mu + 4)) ** 2 592 | for (let j = 1; j <= mu; ++j) prod *= errx / j 593 | errfac[mu] = prod 594 | } 595 | iPt = 0 596 | } 597 | // check return value and abandon integration if called for 598 | if (false === solOut(nAccept + 1, xOld, x, y.slice(1))) { 599 | return Outcome.EarlyReturn 600 | } 601 | } 602 | let err = 0 603 | let errOld = 1e10 604 | let hoptde = posneg * hMax 605 | const w = Solver.dim(km) 606 | w[1] = 0 607 | let reject = false 608 | let last = false 609 | let atov: boolean 610 | let kc = 0 611 | 612 | enum STATE { 613 | Start, BasicIntegrationStep, ConvergenceStep, HopeForConvergence, Accept, Reject 614 | } 615 | let state: STATE = STATE.Start 616 | 617 | loop: while (true) { 618 | this.debug && console.log('STATE', STATE[state], nStep, xOld, x, h, k, kc, hoptde) 619 | switch (state) { 620 | case STATE.Start: 621 | atov = false 622 | // Is xEnd reached in the next step? 623 | if (0.1 * Math.abs(xEnd - x) <= Math.abs(x) * this.uRound) break loop 624 | h = posneg * Math.min(Math.abs(h), Math.abs(xEnd - x), hMax, Math.abs(hoptde)) 625 | if ((x + 1.01 * h - xEnd) * posneg > 0) { 626 | h = xEnd - x 627 | last = true 628 | } 629 | if (nStep === 0 || !this.denseOutput) { 630 | F(x, y, dz) 631 | ++nEval 632 | } 633 | // The first and last step 634 | if (nStep === 0 || last) { 635 | iPt = 0 636 | ++nStep 637 | for (let j = 1; j <= k; ++j) { 638 | kc = j 639 | midex(j) 640 | if (atov) continue loop 641 | if (j > 1 && err <= 1) { 642 | state = STATE.Accept 643 | continue loop 644 | } 645 | } 646 | state = STATE.HopeForConvergence 647 | continue 648 | } 649 | state = STATE.BasicIntegrationStep 650 | continue 651 | 652 | case STATE.BasicIntegrationStep: 653 | // basic integration step 654 | iPt = 0 655 | ++nStep 656 | if (nStep >= this.maxSteps) { 657 | return Outcome.MaxStepsExceeded 658 | } 659 | kc = k - 1 660 | for (let j = 1; j <= kc; ++j) { 661 | midex(j) 662 | if (atov) { 663 | state = STATE.Start 664 | continue loop 665 | } 666 | } 667 | // convergence monitor 668 | if (k === 2 || reject) { 669 | state = STATE.ConvergenceStep 670 | } else { 671 | if (err <= 1) { 672 | state = STATE.Accept 673 | } else if (err > ((nj[k + 1] * nj[k]) / 4) ** 2) { 674 | state = STATE.Reject 675 | } else state = STATE.ConvergenceStep 676 | } 677 | continue 678 | 679 | case STATE.ConvergenceStep: // label 50 680 | midex(k) 681 | if (atov) { 682 | state = STATE.Start 683 | continue 684 | } 685 | kc = k 686 | if (err <= 1) { 687 | state = STATE.Accept 688 | continue 689 | } 690 | state = STATE.HopeForConvergence 691 | continue 692 | 693 | case STATE.HopeForConvergence: 694 | // hope for convergence in line k + 1 695 | if (err > (nj[k + 1] / 2) ** 2) { 696 | state = STATE.Reject 697 | continue 698 | } 699 | kc = k + 1 700 | midex(kc) 701 | if (atov) state = STATE.Start 702 | else if (err > 1) state = STATE.Reject 703 | else state = STATE.Accept 704 | continue 705 | 706 | case STATE.Accept: 707 | if (!acceptStep(this.n)) return Outcome.EarlyReturn 708 | state = STATE.Start 709 | continue 710 | 711 | case STATE.Reject: 712 | k = Math.min(k, kc, km - 1) 713 | if (k > 2 && w[k - 1] < w[k] * this.stepSizeFac3) k -= 1 714 | ++nReject 715 | h = posneg * hh[k] 716 | reject = true 717 | state = STATE.BasicIntegrationStep 718 | } 719 | } 720 | return Outcome.Converged 721 | } 722 | 723 | const outcome = odxcor() 724 | return { 725 | y: y.slice(1), 726 | outcome: outcome, 727 | nStep: nStep, 728 | xEnd: xEnd, 729 | nAccept: nAccept, 730 | nReject: nReject, 731 | nEval: nEval 732 | } 733 | } 734 | } 735 | -------------------------------------------------------------------------------- /src/tonic-example.js: -------------------------------------------------------------------------------- 1 | var odex = require("odex"); 2 | var s = new odex.Solver(2); 3 | var eq = function(x, y) { 4 | return [y[1], -y[0]]; 5 | }; 6 | // This is y'' = -y, y(0) = 1, y'(0) = 0, e.g. cos(x) 7 | s.solve(eq, 0, [1, 0], Math.PI); 8 | // We observe that at x = π, y is near -1 and y' is near 0, 9 | // as expected. 10 | -------------------------------------------------------------------------------- /test/odexTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * An implementation of ODEX, by E. Hairer and G. Wanner, ported from the Fortran ODEX.F. 4 | * The original work carries the BSD 2-clause license, and so does this. 5 | * 6 | * Copyright (c) 2016 Colin Smith. 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | * disclaimer. 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 10 | * following disclaimer in the documentation and/or other materials provided with the distribution. 11 | * 12 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 13 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 14 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 15 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 16 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 17 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 18 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | */ 20 | Object.defineProperty(exports, "__esModule", { value: true }); 21 | var odex_1 = require("../src/odex"); 22 | var assert = require("power-assert"); 23 | describe('Odex', function () { 24 | var NewSolver = function (n) { 25 | var s = new odex_1.Solver(n); 26 | s.maxSteps = 200; 27 | return s; 28 | }; 29 | var airy = function (x, y) { return [y[1], x * y[0]]; }; 30 | var vanDerPol = function (e) { return function (x, y) { return [ 31 | y[1], 32 | ((1 - Math.pow(y[0], 2)) * y[1] - y[0]) / e 33 | ]; }; }; 34 | var bessel = function (a) { return function (x, y) { 35 | var xsq = x * x; 36 | return [y[1], ((a * a - xsq) * y[0] - x * y[1]) / xsq]; 37 | }; }; 38 | var lotkaVolterra = function (a, b, c, d) { return function (x, y) { return [ 39 | a * y[0] - b * y[0] * y[1], 40 | c * y[0] * y[1] - d * y[1] 41 | ]; }; }; 42 | var trig = function (x, y) { return [y[1], -y[0]]; }; 43 | describe('stepSizeSequence', function () { 44 | it('is correct for Type 1', function () { return assert.deepEqual([0, 2, 4, 6, 8, 10, 12, 14, 16], odex_1.Solver.stepSizeSequence(1, 8)); }); 45 | it('is correct for Type 2', function () { return assert.deepEqual([0, 2, 4, 8, 12, 16, 20, 24, 28], odex_1.Solver.stepSizeSequence(2, 8)); }); 46 | it('is correct for Type 3', function () { return assert.deepEqual([0, 2, 4, 6, 8, 12, 16, 24, 32], odex_1.Solver.stepSizeSequence(3, 8)); }); 47 | it('is correct for Type 4', function () { return assert.deepEqual([0, 2, 6, 10, 14, 18, 22, 26, 30], odex_1.Solver.stepSizeSequence(4, 8)); }); 48 | it('is correct for Type 5', function () { return assert.deepEqual([0, 4, 8, 12, 16, 20, 24, 28, 32], odex_1.Solver.stepSizeSequence(5, 8)); }); 49 | it('throws for a bad Type', function () { return assert.throws(function () { return odex_1.Solver.stepSizeSequence(6, 8); }, Error); }); 50 | it('throws for a bad Type', function () { return assert.throws(function () { return odex_1.Solver.stepSizeSequence(0, 8); }, Error); }); 51 | }); 52 | describe('Van der Pol equation w/o dense output', function () { 53 | var s = NewSolver(2); 54 | var tol = 1e-5; 55 | s.absoluteTolerance = s.relativeTolerance = tol; 56 | s.initialStepSize = 0.01; 57 | s.maxSteps = 50; 58 | var y0 = [2, 0]; 59 | var _a = s.solve(vanDerPol(0.1), 0, y0, 2), _b = _a.y, y1 = _b[0], y1p = _b[1], outcome = _a.outcome; 60 | it('converged', function () { return assert.equal(outcome, odex_1.Outcome.Converged); }); 61 | it('worked for y', function () { return assert(Math.abs(y1 + 1.58184) < tol * 10); }); 62 | it("worked for y'", function () { return assert(Math.abs(y1p - 0.978449) < tol * 10); }); 63 | }); 64 | describe("y' = y, (exp)", function () { 65 | var s = NewSolver(1); 66 | var tol = 1e-8; 67 | s.absoluteTolerance = s.relativeTolerance = tol; 68 | var y0 = [1]; 69 | var _a = s.solve(function (x, y) { return y; }, 0, y0, 1), y1 = _a.y[0], outcome = _a.outcome; 70 | it('converged', function () { return assert.equal(outcome, odex_1.Outcome.Converged); }); 71 | it('worked for y', function () { return assert(Math.abs(y1 - Math.exp(1)) < tol * 10); }); 72 | }); 73 | describe('y" = -y (sine/cosine)', function () { 74 | var s = NewSolver(2); 75 | var y0 = [0, 1]; 76 | var _a = s.solve(trig, 0, y0, 1), _b = _a.y, y1 = _b[0], y1p = _b[1], outcome = _a.outcome; 77 | it('converged', function () { return assert.equal(outcome, odex_1.Outcome.Converged); }); 78 | it('worked for y', function () { return assert(Math.abs(y1 - Math.sin(1)) < 1e-5); }); 79 | it("worked for y'", function () { return assert(Math.abs(y1p - Math.cos(1)) < 1e-5); }); 80 | var c = s.solve(trig, 0, y0, 10); 81 | it('converged: long range', function () { return assert.equal(c.outcome, odex_1.Outcome.Converged); }); 82 | it('worked for y', function () { return assert(Math.abs(c.y[0] - Math.sin(10)) < 1e-4); }); 83 | it("worked for y'", function () { return assert(Math.abs(c.y[1] - Math.cos(10)) < 1e-4); }); 84 | }); 85 | describe('Airy equation y" = xy', function () { 86 | var s = NewSolver(2); 87 | s.initialStepSize = 1e-4; 88 | var y0 = [0.3550280539, -0.2588194038]; 89 | var a = s.solve(airy, 0, y0, 1); 90 | it('worked', function () { return assert(a.outcome === odex_1.Outcome.Converged); }); 91 | it('1st kind: works for y', function () { return assert(Math.abs(a.y[0] - 0.1352924163) < 1e-5); }); 92 | it("1st kind: works for y'", function () { return assert(Math.abs(a.y[1] + 0.1591474413) < 1e-5); }); 93 | // Airy equation of the second kind (or "Bairy equation"); this has different 94 | // initial conditions 95 | y0 = [0.6149266274, 0.4482883574]; 96 | var b = s.solve(airy, 0, y0, 1); 97 | it('worked', function () { return assert(b.outcome === odex_1.Outcome.Converged); }); 98 | it('2nd kind: works for y', function () { return assert(Math.abs(b.y[0] - 1.207423595) < 1e-5); }); 99 | it("2nd kind: works for y'", function () { return assert.ok(Math.abs(b.y[1] - 0.9324359334) < 1e-5); }); 100 | }); 101 | describe('Bessel equation x^2 y" + x y\' + (x^2-a^2) y = 0', function () { 102 | var s = NewSolver(2); 103 | var y1 = [0.4400505857, 0.3251471008]; 104 | var y2 = s.solve(bessel(1), 1, y1, 2); 105 | it('converged', function () { return assert(y2.outcome === odex_1.Outcome.Converged); }); 106 | it('y', function () { return assert(Math.abs(y2.y[0] - 0.5767248078) < 1e-5); }); 107 | it("y\"", function () { return assert(Math.abs(y2.y[1] + 0.06447162474) < 1e-5); }); 108 | s.initialStepSize = 1e-6; 109 | var y3 = s.solve(bessel(1), 1, y1, 2); 110 | it('converged', function () { return assert(y3.outcome === odex_1.Outcome.Converged); }); 111 | it('y (small step size)', function () { return assert(Math.abs(y3.y[0] - 0.5767248078) < 1e-6); }); 112 | it("y' (small step size)", function () { return assert(Math.abs(y3.y[1] + 0.06447162474) < 1e-6); }); 113 | s.absoluteTolerance = s.relativeTolerance = 1e-12; 114 | var y4 = s.solve(bessel(1), 1, y1, 2); 115 | it('converged', function () { return assert(y4.outcome === odex_1.Outcome.Converged); }); 116 | it('y (low tolerance)', function () { return assert(Math.abs(y4.y[0] - 0.5767248078) < 1e-10); }); 117 | it('y\' (low tolerance)', function () { return assert(Math.abs(y4.y[1] + 0.06447162474) < 1e-10); }); 118 | }); 119 | describe('max step control', function () { 120 | var s = NewSolver(2); 121 | s.maxSteps = 2; 122 | var o = s.solve(vanDerPol(0.1), 0, [2, 0], 10); 123 | it('didn\' t converge', function () { return assert(o.outcome === odex_1.Outcome.MaxStepsExceeded); }); 124 | it('tried', function () { return assert(o.nStep === s.maxSteps); }); 125 | }); 126 | describe('exits early when asked to', function () { 127 | var s = NewSolver(1); 128 | var evalLimit = 3; 129 | var evalCount = 0; 130 | var o = s.solve(function (x, y) { return [y[0]]; }, 0, [1], 1, function () { 131 | if (++evalCount === evalLimit) 132 | return false; 133 | }); 134 | it('noticed the early exit', function () { return assert(o.outcome === odex_1.Outcome.EarlyReturn); }); 135 | it('took the right number of steps', function () { return assert(o.nStep === evalLimit - 1); }); 136 | var t = NewSolver(1); 137 | var evalCount2 = 0; 138 | t.denseOutput = true; 139 | var o2 = t.solve(function (x, y) { return y; }, 0, [1], 1, t.grid(0.01, function () { 140 | if (++evalCount2 === evalLimit) 141 | return false; 142 | })); 143 | it('noticed the early exit using grid', function () { return assert(o2.outcome === odex_1.Outcome.EarlyReturn); }); 144 | it('took fewer than expected steps using grid', function () { return assert(o2.nStep < 10); }); 145 | }); 146 | describe('cosine (observer)', function () { 147 | var s = NewSolver(2); 148 | var o = s.solve(trig, 0, [1, 0], 2 * Math.PI, function (n, xOld, x, y) { 149 | it('is accurate at grid point ' + n, function () { return assert(Math.abs(y[0] - Math.cos(x)) < 1e-4); }); 150 | // console.log('observed cos', Math.abs(y[0]-Math.cos(x))) 151 | }); 152 | it('converged', function () { return assert(o.outcome === odex_1.Outcome.Converged); }); 153 | }); 154 | describe('sine (observer)', function () { 155 | var s = NewSolver(2); 156 | var o = s.solve(trig, 0, [0, 1], 2 * Math.PI, function (n, xOld, x, y) { 157 | it('is accurate at grid point ' + n, function () { return assert(Math.abs(y[0] - Math.sin(x)) < 1e-5); }); 158 | }); 159 | it('converged', function () { return assert(o.outcome === odex_1.Outcome.Converged); }); 160 | }); 161 | describe('cosine (dense output)', function () { 162 | var s = NewSolver(2); 163 | s.denseOutput = true; 164 | var o = s.solve(trig, 0, [1, 0], 2 * Math.PI, function () { 165 | // console.log('dense cos', Math.abs(y[0]-Math.cos(x))) 166 | }); 167 | it('converged', function () { return assert(o.outcome === odex_1.Outcome.Converged); }); 168 | }); 169 | describe('cosine (dense output, no error estimation)', function () { 170 | var s = NewSolver(2); 171 | s.denseOutput = true; 172 | s.denseOutputErrorEstimator = false; 173 | var o = s.solve(trig, 0, [1, 0], 2 * Math.PI, function () { 174 | // console.log('dense cos n.e.', Math.abs(y[0]-Math.cos(x))) 175 | }); 176 | it('converged', function () { return assert(o.outcome === odex_1.Outcome.Converged); }); 177 | it('evaluated f the correct number of times', function () { return assert(o.nEval === 183); }); 178 | it('took the correct number of steps', function () { return assert(o.nStep === 8); }); 179 | it('had no rejection steps', function () { return assert(o.nReject === 0); }); 180 | }); 181 | describe('cosine (dense output, grid evaluation)', function () { 182 | var s = NewSolver(2); 183 | s.denseOutput = true; 184 | var grid = 0.1; 185 | var current = 0.0; 186 | var o = s.solve(trig, 0, [1, 0], Math.PI / 2, function (n, xOld, x, y, f) { 187 | var _loop_1 = function () { 188 | var k = current; 189 | var v = f(0, current); 190 | var vp = f(1, current); 191 | // console.log('eval', xOld, x, current, v, Math.abs(v-Math.cos(current))) 192 | it('is accurate at interpolated grid point', function () { return assert(Math.abs(v - Math.cos(k)) < 1e-5); }); 193 | it('derivative is accurate at interpolated grid point', function () { return assert(Math.abs(vp + Math.sin(k)) < 1e-5); }); 194 | current += grid; 195 | }; 196 | while (current >= xOld && current < x) { 197 | _loop_1(); 198 | } 199 | }); 200 | it('converged', function () { return assert(o.outcome === odex_1.Outcome.Converged); }); 201 | it('evaluated f the correct number of times', function () { return assert(o.nEval === 101); }); 202 | it('took the correct number of steps', function () { return assert(o.nStep === 7); }); 203 | it('had no rejection steps', function () { return assert(o.nReject === 0); }); 204 | }); 205 | describe('cosine (observer, long range)', function () { 206 | var s = NewSolver(2); 207 | s.denseOutput = false; 208 | var o = s.solve(trig, 0, [1, 0], 16 * Math.PI, function (n, xOld, x, y) { 209 | it('is accurate at grid point ' + n, function () { return assert(Math.abs(y[0] - Math.cos(x)) < 2e-4); }); 210 | // console.log('observed cos l.r.', n, x, y[0], Math.abs(y[0]-Math.cos(x))) 211 | }); 212 | it('converged', function () { return assert(o.outcome === odex_1.Outcome.Converged); }); 213 | it('evaluated f the correct number of times', function () { return assert(o.nEval === 920); }); 214 | it('took the correct number of steps', function () { return assert(o.nStep === 34); }); 215 | it('had no rejection steps', function () { return assert(o.nReject === 0); }); 216 | }); 217 | describe('bogus parameters', function () { 218 | it('throws if maxSteps is <= 0', function () { 219 | var s = NewSolver(2); 220 | s.maxSteps = -2; 221 | assert.throws(function () { 222 | s.solve(trig, 0, [1, 0], 1); 223 | }, Error); 224 | }); 225 | it('throws if maxExtrapolationColumns is <= 2', function () { 226 | var s = NewSolver(2); 227 | s.maxExtrapolationColumns = 1; 228 | assert.throws(function () { 229 | s.solve(trig, 0, [1, 0], 1); 230 | }, Error); 231 | }); 232 | it('throws for dense-output-incompatible step sequence', function () { 233 | var s = NewSolver(2); 234 | s.stepSizeSequence = 1; 235 | s.denseOutput = true; 236 | assert.throws(function () { 237 | s.solve(trig, 0, [1, 0], 1); 238 | }, Error); 239 | }); 240 | it('throws when dense output is requested but no observer function is given', function () { 241 | var s = NewSolver(2); 242 | s.denseOutput = true; 243 | assert.throws(function () { 244 | s.solve(trig, 0, [1, 0], 1); 245 | }, Error); 246 | }); 247 | it('throws for bad interpolation formula degree', function () { 248 | var s = NewSolver(2); 249 | s.interpolationFormulaDegree = 99; 250 | assert.throws(function () { 251 | s.solve(trig, 0, [1, 0], 1); 252 | }, Error); 253 | }); 254 | it('throws for bad uRound', function () { 255 | var s = NewSolver(1); 256 | s.uRound = Math.PI; 257 | assert.throws(function () { 258 | s.solve(trig, 0, [1, 0], 1); 259 | }, Error); 260 | }); 261 | it('throws for bad dense component', function () { 262 | var s = NewSolver(2); 263 | s.denseOutput = true; 264 | s.denseComponents = [5]; 265 | assert.throws(function () { 266 | s.solve(trig, 0, [1, 0], 1, function () { return undefined; }); 267 | }, Error); 268 | }); 269 | }); 270 | describe('requesting specific dense output component', function () { 271 | var s = NewSolver(2); 272 | s.denseComponents = [1]; // we only want y', e.g., -sin(x), densely output 273 | s.denseOutput = true; 274 | var component = function (k) { 275 | var diff = 1e10; 276 | s.solve(trig, 0, [1, 0], 1, function (n, xOld, x, y, f) { 277 | if (x > 0) { 278 | var xh = (x - xOld) / 2; 279 | diff = Math.abs(f(k, xh) + Math.sin(xh)); 280 | return false; 281 | } 282 | }); 283 | return diff; 284 | }; 285 | it('works for the selected component', function () { return assert(component(1) < 1e-5); }); 286 | it('throws for unselected component', function () { return assert.throws(function () { return component(0); }, Error); }); 287 | }); 288 | describe('lotka-volterra equations', function () { 289 | // Validation data from Mathematica: 290 | // LV[a_, b_, c_, d_] := 291 | // NDSolve[{y1'[x] == a y1[x] - b y1[x] y2[x], 292 | // y2'[x] == c y1[x] y2[x] - d y2[x], 293 | // y1[0] == 1, 294 | // y2[0] == 1}, 295 | // {y1, y2}, {x, 0, 25}] 296 | // Table[{y1[t], y2[t]} /. LV[2/3, 4/3, 1, 1], {t, 0, 15}] 297 | var data = [ 298 | [1., 1.], 299 | [0.574285, 0.777439], 300 | [0.489477, 0.47785], 301 | [0.576685, 0.296081], 302 | [0.80643, 0.2148], 303 | [1.19248, 0.211939], 304 | [1.65428, 0.325282], 305 | [1.69637, 0.684714], 306 | [1.01791, 0.999762], 307 | [0.580062, 0.786245], 308 | [0.489149, 0.484395], 309 | [0.572558, 0.299455], 310 | [0.798319, 0.215934], 311 | [1.18032, 0.21089], 312 | [1.64389, 0.319706], 313 | [1.70715, 0.672033] 314 | ]; 315 | var s = NewSolver(2); 316 | s.denseOutput = true; 317 | var i = 0; 318 | s.solve(lotkaVolterra(2 / 3, 4 / 3, 1, 1), 0, [1, 1], 15, s.grid(1, function (x, y) { 319 | var diff = Math.abs(y[0] - data[i][0]); 320 | it('works for y1 at grid point ' + i, function () { return assert(diff < 1e-4); }); 321 | ++i; 322 | })); 323 | }); 324 | describe("Topologist's sine function", function () { 325 | // Here we supply a differential equation designed to test the limits. 326 | // Let y = sin(1/x). Then y' = -cos(1/x) / x^2. 327 | var left = 0.005; 328 | var s = NewSolver(1); 329 | s.denseOutput = true; 330 | s.absoluteTolerance = s.relativeTolerance = [1e-6]; 331 | var o = s.solve(function (x, y) { return [-Math.cos(1 / x) / (x * x)]; }, left, [Math.sin(1 / left)], 2, s.grid(0.1, function (x, y) { 332 | var diff = Math.abs(y[0] - Math.sin(1 / x)); 333 | it('works for y at grid point ' + x, function () { return assert(diff < 1e-4); }); 334 | })); 335 | it('rejected some steps', function () { return assert(o.nReject > 0); }); 336 | }); 337 | describe('Configuration debugging', function () { 338 | it('throws when you use grid without denseOutput', function () { 339 | var s = NewSolver(1); 340 | assert.throws(function () { 341 | s.solve(function (x, y) { return y; }, 0, [1], 1, s.grid(0.1, function (x, y) { 342 | console.log(x, y); 343 | })); 344 | }, /denseOutput/, 'expected recommendation to use denseOutput'); 345 | }); 346 | }); 347 | }); 348 | //# sourceMappingURL=odexTest.js.map -------------------------------------------------------------------------------- /test/odexTest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An implementation of ODEX, by E. Hairer and G. Wanner, ported from the Fortran ODEX.F. 3 | * The original work carries the BSD 2-clause license, and so does this. 4 | * 5 | * Copyright (c) 2016 Colin Smith. 6 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | * disclaimer. 8 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 9 | * following disclaimer in the documentation and/or other materials provided with the distribution. 10 | * 11 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 12 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 13 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 14 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 15 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 16 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 17 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 | */ 19 | 20 | import {Solver, Outcome, Derivative} from '../src/odex' 21 | import assert = require('power-assert') 22 | 23 | describe('Odex', () => { 24 | let NewSolver = (n: number) => { 25 | let s = new Solver(n) 26 | s.maxSteps = 200 27 | return s 28 | } 29 | 30 | let airy: Derivative = (x: number, y: number[]) => [y[1], x * y[0]] 31 | 32 | let vanDerPol: (e: number) => Derivative = e => (x, y) => [ 33 | y[1], 34 | ((1 - Math.pow(y[0], 2)) * y[1] - y[0]) / e 35 | ] 36 | 37 | let bessel: (a: number) => Derivative = (a) => (x, y) => { 38 | let xsq = x * x 39 | return [y[1], ((a * a - xsq) * y[0] - x * y[1]) / xsq] 40 | } 41 | 42 | let lotkaVolterra: (a: number, b: number, c: number, d: number) => Derivative = (a, b, c, d) => (x, y) => [ 43 | a * y[0] - b * y[0] * y[1], 44 | c * y[0] * y[1] - d * y[1] 45 | ] 46 | 47 | let trig: Derivative = (x, y) => [y[1], -y[0]] 48 | 49 | describe('stepSizeSequence', () => { 50 | it('is correct for Type 1', () => assert.deepEqual([0, 2, 4, 6, 8, 10, 12, 14, 16], Solver.stepSizeSequence(1, 8))) 51 | it('is correct for Type 2', () => assert.deepEqual([0, 2, 4, 8, 12, 16, 20, 24, 28], Solver.stepSizeSequence(2, 8))) 52 | it('is correct for Type 3', () => assert.deepEqual([0, 2, 4, 6, 8, 12, 16, 24, 32], Solver.stepSizeSequence(3, 8))) 53 | it('is correct for Type 4', () => assert.deepEqual([0, 2, 6, 10, 14, 18, 22, 26, 30], Solver.stepSizeSequence(4, 8))) 54 | it('is correct for Type 5', () => assert.deepEqual([0, 4, 8, 12, 16, 20, 24, 28, 32], Solver.stepSizeSequence(5, 8))) 55 | it('throws for a bad Type', () => assert.throws(() => Solver.stepSizeSequence(6, 8), Error)) 56 | it('throws for a bad Type', () => assert.throws(() => Solver.stepSizeSequence(0, 8), Error)) 57 | }) 58 | describe('Van der Pol equation w/o dense output', () => { 59 | const s = NewSolver(2) 60 | const tol = 1e-5 61 | s.absoluteTolerance = s.relativeTolerance = tol 62 | s.initialStepSize = 0.01 63 | s.maxSteps = 50 64 | const y0 = [2, 0] 65 | const {y: [y1, y1p], outcome: outcome} = s.solve(vanDerPol(0.1), 0, y0, 2) 66 | it('converged', () => assert.equal(outcome, Outcome.Converged)) 67 | it('worked for y', () => assert(Math.abs(y1 + 1.58184) < tol * 10)) 68 | it(`worked for y'`, () => assert(Math.abs(y1p - 0.978449) < tol * 10)) 69 | }) 70 | describe(`y' = y, (exp)`, () => { 71 | let s = NewSolver(1) 72 | const tol = 1e-8 73 | s.absoluteTolerance = s.relativeTolerance = tol 74 | let y0 = [1] 75 | let {y: [y1], outcome: outcome} = s.solve((x, y) => y, 0, y0, 1) 76 | it('converged', () => assert.equal(outcome, Outcome.Converged)) 77 | it('worked for y', () => assert(Math.abs(y1 - Math.exp(1)) < tol * 10)) 78 | }) 79 | describe('y" = -y (sine/cosine)', () => { 80 | let s = NewSolver(2) 81 | let y0 = [0, 1] 82 | let {y: [y1, y1p], outcome: outcome} = s.solve(trig, 0, y0, 1) 83 | it('converged', () => assert.equal(outcome, Outcome.Converged)) 84 | it('worked for y', () => assert(Math.abs(y1 - Math.sin(1)) < 1e-5)) 85 | it(`worked for y'`, () => assert(Math.abs(y1p - Math.cos(1)) < 1e-5)) 86 | 87 | let c = s.solve(trig, 0, y0, 10) 88 | it('converged: long range', () => assert.equal(c.outcome, Outcome.Converged)) 89 | it('worked for y', () => assert(Math.abs(c.y[0] - Math.sin(10)) < 1e-4)) 90 | it(`worked for y'`, () => assert(Math.abs(c.y[1] - Math.cos(10)) < 1e-4)) 91 | }) 92 | describe('Airy equation y" = xy', () => { 93 | let s = NewSolver(2) 94 | s.initialStepSize = 1e-4 95 | let y0 = [0.3550280539, -0.2588194038] 96 | let a = s.solve(airy, 0, y0, 1) 97 | it('worked', () => assert(a.outcome === Outcome.Converged)) 98 | it('1st kind: works for y', () => assert(Math.abs(a.y[0] - 0.1352924163) < 1e-5)) 99 | it(`1st kind: works for y'`, () => assert(Math.abs(a.y[1] + 0.1591474413) < 1e-5)) 100 | // Airy equation of the second kind (or "Bairy equation"); this has different 101 | // initial conditions 102 | y0 = [0.6149266274, 0.4482883574] 103 | let b = s.solve(airy, 0, y0, 1) 104 | it('worked', () => assert(b.outcome === Outcome.Converged)) 105 | it('2nd kind: works for y', () => assert(Math.abs(b.y[0] - 1.207423595) < 1e-5)) 106 | it(`2nd kind: works for y'`, () => assert.ok(Math.abs(b.y[1] - 0.9324359334) < 1e-5)) 107 | }) 108 | describe('Bessel equation x^2 y" + x y\' + (x^2-a^2) y = 0', () => { 109 | let s = NewSolver(2) 110 | let y1 = [0.4400505857, 0.3251471008] 111 | let y2 = s.solve(bessel(1), 1, y1, 2) 112 | it('converged', () => assert(y2.outcome === Outcome.Converged)) 113 | it('y', () => assert(Math.abs(y2.y[0] - 0.5767248078) < 1e-5)) 114 | it(`y"`, () => assert(Math.abs(y2.y[1] + 0.06447162474) < 1e-5)) 115 | s.initialStepSize = 1e-6 116 | let y3 = s.solve(bessel(1), 1, y1, 2) 117 | it('converged', () => assert(y3.outcome === Outcome.Converged)) 118 | it('y (small step size)', () => assert(Math.abs(y3.y[0] - 0.5767248078) < 1e-6)) 119 | it(`y' (small step size)`, () => assert(Math.abs(y3.y[1] + 0.06447162474) < 1e-6)) 120 | s.absoluteTolerance = s.relativeTolerance = 1e-12 121 | let y4 = s.solve(bessel(1), 1, y1, 2) 122 | it('converged', () => assert(y4.outcome === Outcome.Converged)) 123 | it('y (low tolerance)', () => assert(Math.abs(y4.y[0] - 0.5767248078) < 1e-10)) 124 | it('y\' (low tolerance)', () => assert(Math.abs(y4.y[1] + 0.06447162474) < 1e-10)) 125 | }) 126 | describe('max step control', () => { 127 | let s = NewSolver(2) 128 | s.maxSteps = 2 129 | let o = s.solve(vanDerPol(0.1), 0, [2, 0], 10) 130 | it('didn\' t converge', () => assert(o.outcome === Outcome.MaxStepsExceeded)) 131 | it('tried', () => assert(o.nStep === s.maxSteps)) 132 | }) 133 | describe('exits early when asked to', () => { 134 | let s = NewSolver(1) 135 | let evalLimit = 3 136 | let evalCount = 0 137 | let o = s.solve((x, y) => [y[0]], 0, [1], 1, () => { 138 | if (++evalCount === evalLimit) return false 139 | }) 140 | it('noticed the early exit', () => assert(o.outcome === Outcome.EarlyReturn)) 141 | it('took the right number of steps', () => assert(o.nStep === evalLimit - 1)) 142 | let t = NewSolver(1) 143 | let evalCount2 = 0 144 | t.denseOutput = true 145 | let o2 = t.solve((x, y) => y, 0, [1], 1, t.grid(0.01, () => { 146 | if (++evalCount2 === evalLimit) return false 147 | })) 148 | it('noticed the early exit using grid', () => assert(o2.outcome === Outcome.EarlyReturn)) 149 | it('took fewer than expected steps using grid', () => assert(o2.nStep < 10)) 150 | }) 151 | describe('cosine (observer)', () => { 152 | let s = NewSolver(2) 153 | let o = s.solve(trig, 0, [1, 0], 2 * Math.PI, (n, xOld, x, y) => { 154 | it('is accurate at grid point ' + n, () => assert(Math.abs(y[0] - Math.cos(x)) < 1e-4)) 155 | // console.log('observed cos', Math.abs(y[0]-Math.cos(x))) 156 | }) 157 | it('converged', () => assert(o.outcome === Outcome.Converged)) 158 | }) 159 | describe('sine (observer)', () => { 160 | let s = NewSolver(2) 161 | let o = s.solve(trig, 0, [0, 1], 2 * Math.PI, (n, xOld, x, y) => { 162 | it('is accurate at grid point ' + n, () => assert(Math.abs(y[0] - Math.sin(x)) < 1e-5)) 163 | }) 164 | it('converged', () => assert(o.outcome === Outcome.Converged)) 165 | }) 166 | describe('cosine (dense output)', () => { 167 | let s = NewSolver(2) 168 | s.denseOutput = true 169 | let o = s.solve(trig, 0, [1, 0], 2 * Math.PI, () => { 170 | // console.log('dense cos', Math.abs(y[0]-Math.cos(x))) 171 | }) 172 | it('converged', () => assert(o.outcome === Outcome.Converged)) 173 | }) 174 | describe('cosine (dense output, no error estimation)', () => { 175 | let s = NewSolver(2) 176 | s.denseOutput = true 177 | s.denseOutputErrorEstimator = false 178 | let o = s.solve(trig, 0, [1, 0], 2 * Math.PI, () => { 179 | // console.log('dense cos n.e.', Math.abs(y[0]-Math.cos(x))) 180 | }) 181 | it('converged', () => assert(o.outcome === Outcome.Converged)) 182 | it('evaluated f the correct number of times', () => assert(o.nEval === 183)) 183 | it('took the correct number of steps', () => assert(o.nStep === 8)) 184 | it('had no rejection steps', () => assert(o.nReject === 0)) 185 | }) 186 | describe('cosine (dense output, grid evaluation)', () => { 187 | let s = NewSolver(2) 188 | s.denseOutput = true 189 | const grid = 0.1 190 | let current = 0.0 191 | let o = s.solve(trig, 0, [1, 0], Math.PI / 2, (n, xOld, x, y, f) => { 192 | while (current >= xOld && current < x) { 193 | let k = current 194 | let v = f(0, current) 195 | let vp = f(1, current) 196 | // console.log('eval', xOld, x, current, v, Math.abs(v-Math.cos(current))) 197 | it('is accurate at interpolated grid point', 198 | () => assert(Math.abs(v - Math.cos(k)) < 1e-5)) 199 | it('derivative is accurate at interpolated grid point', 200 | () => assert(Math.abs(vp + Math.sin(k)) < 1e-5)) 201 | current += grid 202 | } 203 | }) 204 | it('converged', () => assert(o.outcome === Outcome.Converged)) 205 | it('evaluated f the correct number of times', () => assert(o.nEval === 101)) 206 | it('took the correct number of steps', () => assert(o.nStep === 7)) 207 | it('had no rejection steps', () => assert(o.nReject === 0)) 208 | }) 209 | describe('cosine (observer, long range)', () => { 210 | let s = NewSolver(2) 211 | s.denseOutput = false 212 | let o = s.solve(trig, 0, [1, 0], 16 * Math.PI, (n, xOld, x, y) => { 213 | it('is accurate at grid point ' + n, () => assert(Math.abs(y[0] - Math.cos(x)) < 2e-4)) 214 | // console.log('observed cos l.r.', n, x, y[0], Math.abs(y[0]-Math.cos(x))) 215 | }) 216 | it('converged', () => assert(o.outcome === Outcome.Converged)) 217 | it('evaluated f the correct number of times', () => assert(o.nEval === 920)) 218 | it('took the correct number of steps', () => assert(o.nStep === 34)) 219 | it('had no rejection steps', () => assert(o.nReject === 0)) 220 | }) 221 | describe('bogus parameters', () => { 222 | it('throws if maxSteps is <= 0', () => { 223 | let s = NewSolver(2) 224 | s.maxSteps = -2 225 | assert.throws(() => { 226 | s.solve(trig, 0, [1, 0], 1) 227 | }, Error) 228 | }) 229 | it('throws if maxExtrapolationColumns is <= 2', () => { 230 | let s = NewSolver(2) 231 | s.maxExtrapolationColumns = 1 232 | assert.throws(() => { 233 | s.solve(trig, 0, [1, 0], 1) 234 | }, Error) 235 | }) 236 | it('throws for dense-output-incompatible step sequence', () => { 237 | let s = NewSolver(2) 238 | s.stepSizeSequence = 1 239 | s.denseOutput = true 240 | assert.throws(() => { 241 | s.solve(trig, 0, [1, 0], 1) 242 | }, Error) 243 | }) 244 | it('throws when dense output is requested but no observer function is given', () => { 245 | let s = NewSolver(2) 246 | s.denseOutput = true 247 | assert.throws(() => { 248 | s.solve(trig, 0, [1, 0], 1) 249 | }, Error) 250 | }) 251 | it('throws for bad interpolation formula degree', () => { 252 | let s = NewSolver(2) 253 | s.interpolationFormulaDegree = 99 254 | assert.throws(() => { 255 | s.solve(trig, 0, [1, 0], 1) 256 | }, Error) 257 | }) 258 | it('throws for bad uRound', () => { 259 | let s = NewSolver(1) 260 | s.uRound = Math.PI 261 | assert.throws(() => { 262 | s.solve(trig, 0, [1, 0], 1) 263 | }, Error) 264 | }) 265 | it('throws for bad dense component', () => { 266 | let s = NewSolver(2) 267 | s.denseOutput = true 268 | s.denseComponents = [5] 269 | assert.throws(() => { 270 | s.solve(trig, 0, [1, 0], 1, () => undefined) 271 | }, Error) 272 | }) 273 | }) 274 | describe('requesting specific dense output component', () => { 275 | let s = NewSolver(2) 276 | s.denseComponents = [1] // we only want y', e.g., -sin(x), densely output 277 | s.denseOutput = true 278 | let component = (k: number) => { 279 | let diff = 1e10 280 | s.solve(trig, 0, [1, 0], 1, (n, xOld, x, y, f) => { 281 | if (x > 0) { 282 | let xh = (x - xOld) / 2 283 | diff = Math.abs(f(k, xh) + Math.sin(xh)) 284 | return false 285 | } 286 | }) 287 | return diff 288 | } 289 | it('works for the selected component', () => assert(component(1) < 1e-5)) 290 | it('throws for unselected component', () => assert.throws(() => component(0), Error)) 291 | }) 292 | describe('lotka-volterra equations', () => { 293 | // Validation data from Mathematica: 294 | // LV[a_, b_, c_, d_] := 295 | // NDSolve[{y1'[x] == a y1[x] - b y1[x] y2[x], 296 | // y2'[x] == c y1[x] y2[x] - d y2[x], 297 | // y1[0] == 1, 298 | // y2[0] == 1}, 299 | // {y1, y2}, {x, 0, 25}] 300 | // Table[{y1[t], y2[t]} /. LV[2/3, 4/3, 1, 1], {t, 0, 15}] 301 | let data = [ 302 | [1., 1.], 303 | [0.574285, 0.777439], 304 | [0.489477, 0.47785], 305 | [0.576685, 0.296081], 306 | [0.80643, 0.2148], 307 | [1.19248, 0.211939], 308 | [1.65428, 0.325282], 309 | [1.69637, 0.684714], 310 | [1.01791, 0.999762], 311 | [0.580062, 0.786245], 312 | [0.489149, 0.484395], 313 | [0.572558, 0.299455], 314 | [0.798319, 0.215934], 315 | [1.18032, 0.21089], 316 | [1.64389, 0.319706], 317 | [1.70715, 0.672033] 318 | ] 319 | let s = NewSolver(2) 320 | s.denseOutput = true 321 | let i = 0 322 | s.solve(lotkaVolterra(2 / 3, 4 / 3, 1, 1), 0, [1, 1], 15, s.grid(1, (x, y) => { 323 | let diff = Math.abs(y[0] - data[i][0]) 324 | it('works for y1 at grid point ' + i, () => assert(diff < 1e-4)) 325 | ++i 326 | })) 327 | }) 328 | describe(`Topologist's sine function`, () => { 329 | // Here we supply a differential equation designed to test the limits. 330 | // Let y = sin(1/x). Then y' = -cos(1/x) / x^2. 331 | const left = 0.005 332 | let s = NewSolver(1) 333 | s.denseOutput = true 334 | s.absoluteTolerance = s.relativeTolerance = [1e-6] 335 | let o = s.solve((x, y) => [-Math.cos(1 / x) / (x * x)], left, [Math.sin(1 / left)], 2, s.grid(0.1, (x, y) => { 336 | let diff = Math.abs(y[0] - Math.sin(1 / x)) 337 | it('works for y at grid point ' + x, () => assert(diff < 1e-4)) 338 | })) 339 | it('rejected some steps', () => assert(o.nReject > 0)) 340 | }) 341 | describe('Configuration debugging', () => { 342 | it ('throws when you use grid without denseOutput', () => { 343 | let s = NewSolver(1) 344 | assert.throws(() => { 345 | s.solve((x, y) => y, 0, [1], 1, s.grid(0.1, (x, y) => { 346 | console.log(x, y) 347 | })) 348 | }, /denseOutput/, 'expected recommendation to use denseOutput') 349 | }) 350 | }) 351 | }) 352 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "sourceMap": true 7 | }, 8 | "files": [ 9 | "src/odex.ts", 10 | "test/odexTest.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "single" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "never" 29 | ], 30 | "triple-equals": [ 31 | true, 32 | "allow-null-check" 33 | ], 34 | "typedef-whitespace": [ 35 | true, 36 | { 37 | "call-signature": "nospace", 38 | "index-signature": "nospace", 39 | "parameter": "nospace", 40 | "property-declaration": "nospace", 41 | "variable-declaration": "nospace" 42 | } 43 | ], 44 | "variable-name": [ 45 | true, 46 | "ban-keywords" 47 | ], 48 | "whitespace": [ 49 | true, 50 | "check-branch", 51 | "check-decl", 52 | "check-operator", 53 | "check-separator", 54 | "check-type" 55 | ] 56 | } 57 | } 58 | --------------------------------------------------------------------------------