├── .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 | [](https://travis-ci.org/littleredcomputer/odex-js) []() []() [](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 |
--------------------------------------------------------------------------------