      1 /*
      2     Copyright 2008-2025
      3         Matthias Ehmann,
      4         Carsten Miller,
      5         Alfred Wassermann
      6
      7     This file is part of JSXGraph.
      8
      9     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     10
     11     You can redistribute it and/or modify it under the terms of the
     12
     13       * GNU Lesser General Public License as published by
     14         the Free Software Foundation, either version 3 of the License, or
     15         (at your option) any later version
     16       OR
     17       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     18
     19     JSXGraph is distributed in the hope that it will be useful,
     20     but WITHOUT ANY WARRANTY; without even the implied warranty of
     21     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     22     GNU Lesser General Public License for more details.
     23
     24     You should have received a copy of the GNU Lesser General Public License and
     25     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
     26     and <https://opensource.org/licenses/MIT/>.
     27  */
     28
     29 "use strict";
     30
     31 import Type from "../utils/type.js";
     32 import Mat from "./math.js";
     33 import Geometry from "./geometry.js";
     34 import Numerics from "./numerics.js";
     35 import Quadtree from "./bqdt.js";
     36
     37 /**
     38  * Plotting of curves which are given implicitly as the set of points solving an equation
     39  * <i>f(x,y) = 0</i>.
     40  * <p>
     41  * The main class initializes a new implicit plot instance.
     42  * <p>
     43  * The algorithm should be able to plot most implicit curves as long as the equations
     44  * are not too complex. We are aware of the paper by Oliver Labs,
     45  * <a href="https://link.springer.com/chapter/10.1007/978-1-4419-0999-2_6">A List of Challenges for Real Algebraic Plane Curve Visualization Software</a>
     46  * which contains many equations where this algorithm may fail.
     47  * For example,  at the time being there is no attempt to detect <i>solitary points</i>.
     48  * Also, it is always a trade off to find all components of the curve and
     49  * keep the construction responsive.
     50  *
     51  * @name JXG.Math.ImplicitPlot
     52  * @exports Mat.ImplicitPlot as JXG.Math.ImplicitPlot
     53  * @param {Array} bbox Bounding box of the area in which solutions of the equation
     54  * are determined.
     55  * @param {Object} config Configuration object. Default:
     56  * <pre>
     57  *  {
     58  *      resolution_out: 5,    // Horizontal resolution: distance between vertical lines to search for components
     59  *      resolution_in: 5,     // Vertical resolution to search for components
     60  *      max_steps: 1024,      // Max number of points in one call of tracing
     61  *      alpha_0: 0.05,        // Angle between two successive tangents: smoothness of curve
     62  *
     63  *      tol_u0: Mat.eps,      // Tolerance to find starting points for tracing.
     64  *      tol_newton: 1.0e-7,   // Tolerance for Newton steps.
     65  *      tol_cusp: 0.05,       // Tolerance for cusp / bifurcation detection
     66  *      tol_progress: 0.0001, // If two points are closer than this value, we bail out
     67  *      qdt_box: 0.2,         // half of box size to search in qdt
     68  *      kappa_0: 0.2,         // Inverse of planned number of Newton steps
     69  *      delta_0: 0.05,        // Distance of predictor point to curve
     70  *
     71  *      h_initial: 0.1,       // Initial stepwidth
     72  *      h_critical: 0.001,    // If h is below this threshold we bail out
     73  *      h_max: 1,             // Maximal value of h (user units)
     74  *      loop_dist: 0.09,      // Allowed distance (multiplied by actual stepwidth) to detect loop
     75  *      loop_dir: 0.99,       // Should be > 0.95
     76  *      loop_detection: true, // Use Gosper's loop detector
     77  *      unitX: 10,            // unitX of board
     78  *      unitY: 10             // unitX of board
     79  *   };
     80  * </pre>
     81  * @param {function} f function from <b>R</b><sup>2</sup> to <b>R</b>
     82  * @param {function} [dfx] Optional partial derivative of <i>f</i> with regard to <i>x</i>
     83  * @param {function} [dfy] Optional partial derivative of <i>f</i> with regard to <i>y</i>
     84  *
     85  * @constructor
     86  * @example
     87  *     var f = (x, y) => x**3 - 2 * x * y + y**3;
     88  *     var c = board.create('curve', [[], []], {
     89  *             strokeWidth: 3,
     90  *             strokeColor: JXG.palette.red
     91  *         });
     92  *
     93  *     c.updateDataArray = function () {
     94  *         var bbox = this.board.getBoundingBox(),
     95  *             ip, cfg,
     96  *             ret = [],
     97  *             mgn = 1;
     98  *
     99  *         bbox[0] -= mgn;
    100  *         bbox[1] += mgn;
    101  *         bbox[2] += mgn;
    102  *         bbox[3] -= mgn;
    103  *
    104  *         cfg = {
    105  *             resolution_out: 5,
    106  *             resolution_in: 5,
    107  *             unitX: this.board.unitX,
    108  *             unitY: this.board.unitX
    109  *         };
    110  *
    111  *         this.dataX = [];
    112  *         this.dataY = [];
    113  *         ip = new JXG.Math.ImplicitPlot(bbox, cfg, f, null, null);
    114  *         ret = ip.plot();
    115  *         this.dataX = ret[0];
    116  *         this.dataY = ret[1];
    117  *     };
    118  *     board.update();
    119  * </pre><div id="JXGf3e8cd82-2b67-4efb-900a-471eb92b3b96" class="jxgbox" style="width: 300px; height: 300px;"></div>
    120  * <script type="text/javascript">
    121  *     (function() {
    122  *         var board = JXG.JSXGraph.initBoard('JXGf3e8cd82-2b67-4efb-900a-471eb92b3b96',
    123  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    124  *             var f = (x, y) => x**3 - 2 * x * y + y**3;
    125  *             var c = board.create('curve', [[], []], {
    126  *                     strokeWidth: 3,
    127  *                     strokeColor: JXG.palette.red
    128  *                 });
    129  *
    130  *             c.updateDataArray = function () {
    131  *                 var bbox = this.board.getBoundingBox(),
    132  *                     ip, cfg,
    133  *                     ret = [],
    134  *                     mgn = 1;
    135  *
    136  *                 bbox[0] -= mgn;
    137  *                 bbox[1] += mgn;
    138  *                 bbox[2] += mgn;
    139  *                 bbox[3] -= mgn;
    140  *
    141  *                 cfg = {
    142  *                     resolution_out: 5,
    143  *                     resolution_in: 5,
    144  *                     unitX: this.board.unitX,
    145  *                     unitY: this.board.unitX
    146  *                 };
    147  *
    148  *                 this.dataX = [];
    149  *                 this.dataY = [];
    150  *
    151  *                 ip = new JXG.Math.ImplicitPlot(bbox, cfg, f, null, null);
    152  *                 ret = ip.plot();
    153  *
    154  *                 this.dataX = ret[0];
    155  *                 this.dataY = ret[1];
    156  *             };
    157  *             board.update();
    158  *
    159  *     })();
    160  *
    161  * </script><pre>
    162  *
    163  */
    164 Mat.ImplicitPlot = function (bbox, config, f, dfx, dfy) {
    165
    166     // Default values
    167     var cfg_default = {
    168         resolution_out: 5,    // Distance between vertical lines to search for components
    169         resolution_in: 5,     // Distance between vertical lines to search for components
    170         max_steps: 1024,      // Max number of points in one call of tracing
    171         alpha_0: 0.05,        // Angle between two successive tangents: smoothness of curve
    172
    173         tol_u0: Mat.eps,      // Tolerance to find starting points for tracing.
    174         tol_newton: 1.0e-7,   // Tolerance for Newton steps.
    175         tol_cusp: 0.05,       // Tolerance for cusp / bifurcation detection
    176         tol_progress: 0.0001, // If two points are closer than this value, we bail out
    177         qdt_box: 0.2,         // half of box size to search in qdt
    178         kappa_0: 0.2,         // Inverse of planned number of Newton steps
    179         delta_0: 0.05,        // Distance of predictor point to curve
    180
    181         h_initial: 0.1,       // Initial step width
    182         h_critical: 0.001,    // If h is below this threshold we bail out
    183         h_max: 1,             // Maximum value of h (user units)
    184         loop_dist: 0.09,      // Allowed distance (multiplied by actual step width) to detect loop
    185         loop_dir: 0.99,       // Should be > 0.95
    186         loop_detection: true, // Use Gosper's loop detector
    187         unitX: 10,            // unitX of board
    188         unitY: 10             // unitX of board
    189     };
    190
    191     this.config = Type.merge(cfg_default, config);
    192
    193     this.f = f;
    194
    195     this.dfx = null;
    196     this.dfy = null;
    197
    198     if (Type.isFunction(dfx)) {
    199         this.dfx = dfx;
    200     } else {
    201         this.dfx = function (x, y) {
    202             var h = Mat.eps * Mat.eps;
    203             return (this.f(x + h, y) - this.f(x - h, y)) * 0.5 / h;
    204         };
    205     }
    206
    207     if (Type.isFunction(dfy)) {
    208         this.dfy = dfy;
    209     } else {
    210         this.dfy = function (x, y) {
    211             var h = Mat.eps * Mat.eps;
    212             return (this.f(x, y + h) - this.f(x, y - h)) * 0.5 / h;
    213         };
    214     }
    215
    216     this.bbox = bbox;
    217     this.qdt = new Quadtree(20, 5, bbox);
    218
    219     this.components = [];
    220 };
    221
    222 Type.extend(
    223     Mat.ImplicitPlot.prototype,
    224     /** @lends JXG.Math.ImplicitPlot.prototype */ {
    225
    226         /**
    227          * Implicit plotting method.
    228          *
    229          * @returns {Array} consisting of [dataX, dataY, number_of_components]
    230          */
    231         plot: function () {
    232             var // components = [],
    233                 doVerticalSearch = true,
    234                 doHorizontalSearch = true,
    235                 x, y,
    236                 mi_x, ma_x, mi_y, ma_y,
    237                 dataX = [],
    238                 dataY = [],
    239                 ret = [],
    240                 num_components = 0,
    241
    242                 delta,
    243                 that = this,
    244
    245                 fmi_x = function (t) {
    246                     return that.f(x, t);
    247                 },
    248                 fma_x = function (t) {
    249                     return -that.f(x, t);
    250                 },
    251                 fmi_y = function (t) {
    252                     return that.f(t, y);
    253                 },
    254                 fma_y = function (t) {
    255                     return -that.f(t, y);
    256                 };
    257
    258             // Vertical lines or circular search:
    259             mi_x = Math.min(this.bbox[0], this.bbox[2]) - Mat.eps;
    260             ma_x = Math.max(this.bbox[0], this.bbox[2]);
    261             mi_y = Math.min(this.bbox[1], this.bbox[3]) + Mat.eps;
    262             ma_y = Math.max(this.bbox[1], this.bbox[3]);
    263
    264             if (doVerticalSearch) {
    265                 delta = this.config.resolution_out / this.config.unitX;
    266                 delta *= (1 + Mat.eps);
    267                 // console.log("Outer delta x", delta)
    268
    269                 for (x = mi_x; x < ma_x; x += delta) {
    270                     ret = this.searchLine(
    271                         fmi_x, fma_x, x,
    272                         [mi_y, ma_y], 'vertical',
    273                         num_components, dataX, dataY, 20);
    274
    275                     if (ret !== false) {
    276                         dataX = ret[0];
    277                         dataY = ret[1];
    278                         num_components = ret[2];
    279                     }
    280
    281                 }
    282             }
    283             if (doHorizontalSearch) {
    284                 delta = this.config.resolution_out / this.config.unitY;
    285                 delta *= (1 + Mat.eps);
    286                 // console.log("Outer delta y", delta)
    287
    288                 for (y = mi_y; y < ma_y; y += delta) {
    289                     ret = this.searchLine(
    290                         fmi_y, fma_y, y,
    291                         [mi_x, ma_x], 'horizontal',
    292                         num_components, dataX, dataY, 20);
    293
    294                     if (ret !== false) {
    295                         dataX = ret[0];
    296                         dataY = ret[1];
    297                         num_components = ret[2];
    298                     }
    299                 }
    300             }
    301
    302             return [dataX, dataY, num_components];
    303         },
    304
    305         /**
    306          * Recursively search a horizontal or vertical line for points on the
    307          * fulfilling the given equation.
    308          *
    309          * @param {Function} fmi Minimization function
    310          * @param {Function} fma Maximization function
    311          * @param {Number} fix Value of the fixed variable
    312          * @param {Array} interval Search interval of the free variable
    313          * @param {String} dir 'vertical' or 'horizontal'
    314          * @param {Number} num_components Number of components before search
    315          * @param {Array} dataX x-coordinates of points so far
    316          * @param {Array} dataY y-coordinates of points so far
    317          * @param {Number} level Recursion level
    318          * @returns {Array} consisting of [dataX, dataY, number_of_components]-
    319          * @private
    320          */
    321         searchLine: function (fmi, fma, fix, interval, dir,
    322             num_components, dataX, dataY, level) {
    323             var t_mi, t_ma, t,
    324                 ft,
    325                 mi, ma, tmp, m,
    326                 is_in,
    327                 u0, i, le,
    328                 ret,
    329                 offset,
    330                 delta,
    331                 eps = this.config.tol_u0,
    332                 DEBUG = false,
    333                 b = interval[0],
    334                 e = interval[1];
    335
    336             t_mi = Numerics.fminbr(fmi, [b, e]);
    337             mi = fmi(t_mi);
    338             t_ma = Numerics.fminbr(fma, [b, e]);
    339             ma = fmi(t_ma);
    340
    341             if (mi < eps && ma > -eps) {
    342                 tmp = t_mi;
    343                 t_mi = Math.min(tmp, t_ma);
    344                 t_ma = Math.max(tmp, t_ma);
    345
    346                 t = Numerics.fzero(fmi, [t_mi, t_ma]);
    347                 // t = Numerics.chandrupatla(fmi, [t_mi, t_ma]);
    348
    349                 ft = fmi(t);
    350                 if (Math.abs(ft) > Math.max((ma - mi) * Mat.eps, 0.001)) {
    351                     //console.log("searchLine:",  dir, fix, t, "no root " + ft);
    352                     return false;
    353                     // throw new Error("searchLine: no root " + ft);
    354                 }
    355                 if (dir === 'vertical') {
    356                     u0 = [1, fix, t];
    357                     delta = this.config.resolution_in / this.config.unitY;
    358                     // console.log("Inner delta x", delta)
    359                 } else {
    360                     u0 = [1, t, fix];
    361                     delta = this.config.resolution_in / this.config.unitX;
    362                     // console.log("Inner delta y", delta)
    363                 }
    364                 delta *= (1 + Mat.eps);
    365
    366                 is_in = this.curveContainsPoint(u0, dataX, dataY,
    367                     delta * 2,           // Allowed dist from segment
    368                     this.config.qdt_box  // 0.5 of box size to search in qdt
    369                 );
    370
    371                 if (is_in) {
    372                     if (DEBUG) {
    373                         console.log("Found in quadtree", u0);
    374                     }
    375                 } else {
    376                     if (DEBUG) {
    377                         console.log("Not in quadtree", u0, dataX.length);
    378                     }
    379                     ret = this.traceComponent(u0, 1);
    380                     if (ret[0].length > 0) {
    381                         // Add jump in curve
    382                         if (num_components > 0) {
    383                             dataX.push(NaN);
    384                             dataY.push(NaN);
    385                         }
    386
    387                         offset = dataX.length;
    388                         le = ret[0].length;
    389                         for (i = 1; i < le; i++) {
    390                             this.qdt.insertItem({
    391                                 xlb: Math.min(ret[0][i - 1], ret[0][i]),
    392                                 xub: Math.max(ret[0][i - 1], ret[0][i]),
    393                                 ylb: Math.min(ret[1][i - 1], ret[1][i]),
    394                                 yub: Math.max(ret[1][i - 1], ret[1][i]),
    395                                 idx1: offset + i - 1,
    396                                 idx2: offset + i,
    397                                 comp: num_components
    398                             });
    399                         }
    400
    401                         num_components++;
    402                         Type.concat(dataX, ret[0]);
    403                         Type.concat(dataY, ret[1]);
    404                     }
    405                 }
    406
    407                 m = t - delta * 0.01;
    408                 if (m - b > delta && level > 0) {
    409                     ret = this.searchLine(
    410                         fmi, fma, fix, [b, m], dir,
    411                         num_components, dataX, dataY, level - 1);
    412                     if (ret !== false) {
    413                         dataX = ret[0];
    414                         dataY = ret[1];
    415                         num_components = ret[2];
    416                     }
    417                 }
    418                 m = t + delta * 0.01;
    419                 if (e - m > delta  && level > 0) {
    420                     ret = this.searchLine(
    421                         fmi, fma, fix, [m, e], dir,
    422                         num_components, dataX, dataY, level - 1);
    423                     if (ret !== false) {
    424                         dataX = ret[0];
    425                         dataY = ret[1];
    426                         num_components = ret[2];
    427                     }
    428                 }
    429
    430                 return [dataX, dataY, num_components];
    431             }
    432
    433             return false;
    434         },
    435
    436         /**
    437          * Test if the data points contain a given coordinate, i.e. if the
    438          * given coordinate is close enough to the polygonal chain
    439          * through the data points.
    440          *
    441          * @param {Array} p Homogenous coordinates [1, x, y] of the coordinate point
    442          * @param {Array} dataX x-coordinates of points so far
    443          * @param {Array} dataY y-coordinates of points so far
    444          * @param {Number} tol Maximal distance of p from the polygonal chain through the data points
    445          * @param {Number} eps Helper tolerance used for the quadtree
    446          * @returns Boolean
    447          */
    448         curveContainsPoint: function (p, dataX, dataY, tol, eps) {
    449             var i, le, hits, d,
    450                 x = p[1],
    451                 y = p[2];
    452
    453             hits = this.qdt.find([x - eps, y + eps, x + eps, y - eps]);
    454
    455             le = hits.length;
    456             for (i = 0; i < le; i++) {
    457                 d = Geometry.distPointSegment(
    458                     p,
    459                     [1, dataX[hits[i].idx1], dataY[hits[i].idx1]],
    460                     [1, dataX[hits[i].idx2], dataY[hits[i].idx2]]
    461                 );
    462                 if (d < tol) {
    463                     return true;
    464                 }
    465             }
    466             return false;
    467         },
    468
    469         /**
    470          * Starting at an initial point the curve is traced with a Euler-Newton method.
    471          * After tracing in one direction the algorithm stops if the component is a closed loop.
    472          * Otherwise, the curved is traced in the opposite direction, starting from
    473          * the same initial point. Finally, the two components are glued together.
    474          *
    475          * @param {Array} u0 Initial point in homogenous coordinates [1, x, y].
    476          * @returns Array [dataX, dataY] containing a new component.
    477          * @private
    478          */
    479         traceComponent: function (u0) {
    480             var dataX = [],
    481                 dataY = [],
    482                 arr = [];
    483
    484             // Trace in first direction
    485             // console.log("---- Start tracing forward ---------")
    486             arr = this.tracing(u0, 1);
    487
    488             if (arr.length === 0) {
    489                 // console.log("Could not start tracing due to singularity")
    490             } else {
    491                 // console.log("Trace from", [arr[0][0], arr[1][0]], "to", [arr[0][arr[0].length - 1], arr[1][arr[1].length - 1]],
    492                 //    "num points:", arr[0].length);
    493                 dataX = arr[0];
    494                 dataY = arr[1];
    495             }
    496
    497             // Trace in the other direction
    498             if (!arr[2]) {
    499                 // No loop in the first tracing step,
    500                 // now explore the other direction.
    501
    502                 // console.log("---- Start tracing backward ---------")
    503                 arr = this.tracing(u0, -1);
    504
    505                 if (arr.length === 0) {
    506                     // console.log("Could not start backward tracing due to singularity")
    507                 } else {
    508                     // console.log("Trace backwards from", [arr[0][0], arr[1][0]], "to",
    509                     //     [arr[0][arr[0].length - 1], arr[1][arr[1].length - 1]], "num points:", arr[0].length);
    510                     dataX = arr[0].reverse().concat(dataX.slice(1));
    511                     dataY = arr[1].reverse().concat(dataY.slice(1));
    512                 }
    513             }
    514
    515             if (dataX.length > 0 && dataX.length < 6) {
    516                 // Solitary point
    517                 dataX.push(dataX[dataX.length - 1]);
    518                 dataY.push(dataY[dataY.length - 1]);
    519             }
    520
    521             return [dataX, dataY];
    522         },
    523
    524         /**
    525          * Starting at a point <i>u0</i>, this routine traces the curve <i>f(u)=0</i> until
    526          * a loop is detected, a critical point is reached, the curve leaves the bounding box,
    527          * or the maximum number of points is reached.
    528          * <p>
    529          * The method is a predictor / corrector method consisting of Euler and Newton steps
    530          * together with step width adaption.
    531          * <p>
    532          * The algorithm is an adaption of the algorithm in
    533          * Eugene L. Allgower, Kurt Georg: <i>Introduction to Numerical Continuation methods.</i>
    534          *
    535          * @param {Array} u0 Starting point in homogenous coordinates  [1, x, y].
    536          * @param {Number} direction 1 or -1
    537          * @returns Array [pathX, pathY, loop_closed] or []
    538          * @private
    539          */
    540         tracing: function (u0, direction) {
    541             var u = [],
    542                 v = [],
    543                 v_start = [],
    544                 w = [],
    545                 t_u, t_v, t_u_0,
    546                 A,
    547                 grad,
    548                 nrm,
    549                 dir,
    550                 steps = 0,
    551                 k = 0,
    552                 loop_closed = false,
    553                 k0, k1, denom, dist, progress,
    554                 kappa, delta, alpha,
    555                 factor,
    556                 point_added = false,
    557
    558                 quasi = false,
    559                 cusp_or_bifurc = false,
    560                 kappa_0 = this.config.kappa_0,  // Inverse of planned number of Newton steps
    561                 delta_0 = this.config.delta_0,  // Distance of predictor point to curve
    562                 alpha_0 = this.config.alpha_0,  // Angle between two successive tangents
    563                 h = this.config.h_initial,
    564                 max_steps = this.config.max_steps,
    565
    566                 omega = direction,
    567                 pathX = [],
    568                 pathY = [],
    569
    570                 T = [],            // Gosper's loop detector table
    571                 n, m, i, e;
    572
    573             u = u0.slice(1);
    574             pathX.push(u[0]);
    575             pathY.push(u[1]);
    576
    577             t_u = this.tangent(u);
    578             if (t_u === false) {
    579                 // We don't want to start at a singularity.
    580                 // Get out of here and search for another starting point.
    581                 return [];
    582             }
    583             A = [this.dfx(u[0], u[1]), this.dfy(u[0], u[1])];
    584
    585             do {
    586
    587                 if (quasi) {
    588                     t_u = this.tangent_A(A);
    589                 } else {
    590                     t_u = this.tangent(u);
    591                 }
    592                 if (t_u === false) {
    593                     u = v.slice();
    594                     pathX.push(u[0]);
    595                     pathY.push(u[1]);
    596                     // console.log("-> Bail out: t_u undefined.");
    597                     break;
    598                 }
    599
    600                 if (pathX.length === 1) {
    601                     // Store first point
    602                     t_u_0 = t_u.slice();
    603                 } else if (pathX.length === 2) {
    604                     T.push(pathX.length - 1);       // Put first point into Gosper table T
    605
    606                 } else if (point_added && pathX.length > 2 && !cusp_or_bifurc) {
    607
    608                     // Detect if loop has been closed
    609                     dist = Geometry.distPointSegment(
    610                         [1, u[0], u[1]],
    611                         [1, pathX[0], pathY[0]],
    612                         [1, pathX[1], pathY[1]]
    613                     );
    614
    615                     if (dist < this.config.loop_dist * h &&
    616                         Mat.innerProduct(t_u, t_u_0, 2) > this.config.loop_dir
    617                     ) {
    618
    619                         // console.log("Loop detected after", steps, "steps");
    620                         // console.log("\t", "v", v, "u0:", u0)
    621                         // console.log("\t", "Dist(v, path0)", dist, config.loop_dist * h)
    622                         // console.log("\t", "t_u", t_u);
    623                         // console.log("\t", "inner:", Mat.innerProduct(t_u, t_u_0, 2));
    624                         // console.log("\t", "h", h);
    625
    626                         u = u0.slice(1);
    627                         pathX.push(u[0]);
    628                         pathY.push(u[1]);
    629
    630                         loop_closed = true;
    631                         break;
    632                     }
    633
    634                     // Gosper's loop detector
    635                     if (this.config.loop_detection) {
    636                         n = pathX.length - 1;
    637                         // console.log("Check Gosper", n);
    638                         m = Math.floor(Mat.log2(n));
    639
    640                         for (i = 0; i <= m; i++) {
    641                             dist = Geometry.distPointSegment(
    642                                 [1, u[0], u[1]],
    643                                 [1, pathX[T[i] - 1], pathY[T[i] - 1]],
    644                                 [1, pathX[T[i]], pathY[T[i]]]
    645                             );
    646
    647                             if (dist < this.config.loop_dist * h) {
    648                                 // console.log("!!!!!!!!!!!!!!! GOSPER LOOP CLOSED !!!!", i, n + 1,
    649                                 //     this.config.loop_dist * h
    650                                 // );
    651
    652                                 t_v = this.tangent([pathX[T[i]], pathY[T[i]]]);
    653                                 if (Mat.innerProduct(t_u, t_v, 2) > this.config.loop_dir) {
    654                                     // console.log("!!!!!!!!!!!!!!! angle is good enough");
    655                                     break;
    656                                 }
    657                             }
    658                         }
    659                         if (i <= m) {
    660                             loop_closed = true;
    661                             break;
    662                         }
    663
    664                         m = 1;
    665                         e = 0;
    666                         for (i = 0; i < 100; i++) {
    667                             if ((n + 1) % m !== 0) {
    668                                 break;
    669                             }
    670                             m *= 2;
    671                             e++;
    672                         }
    673                         // console.log("Add at e", e);
    674                         T[e] = n;
    675                     }
    676
    677                 }
    678
    679                 // Predictor step
    680                 // if (true /*h < 2 * this.config.h_initial*/) {
    681                 // Euler
    682                 // console.log("euler")
    683                 v[0] = u[0] + h * omega * t_u[0];
    684                 v[1] = u[1] + h * omega * t_u[1];
    685                 // } else {
    686                 //     // Heun
    687                 //     // console.log("heun")
    688                 //     v[0] = u[0] + h * omega * t_u[0];
    689                 //     v[1] = u[1] + h * omega * t_u[1];
    690
    691                 //     t_v = this.tangent(v);
    692                 //     v[0] = 0.5 * u[0] + 0.5 * (v[0] + h * omega * t_v[0]);
    693                 //     v[1] = 0.5 * u[1] + 0.5 * (v[1] + h * omega * t_v[1]);
    694                 // }
    695                 if (quasi) {
    696                     A = this.updateA(A, u, v);
    697                     v_start = v.slice();
    698                 }
    699
    700                 // Corrector step: Newton
    701                 k = 0;
    702                 do {
    703                     if (quasi) {
    704                         grad = A;
    705                     } else {
    706                         grad = [this.dfx(v[0], v[1]), this.dfy(v[0], v[1])];
    707                     }
    708
    709                     // Compute w = v - A(v) * f(v),
    710                     // grad: row vector and A(v) is the Moore-Penrose inverse:
    711                     // grad^T * (grad * grad^T)^(-1)
    712                     denom = grad[0] * grad[0] + grad[1] * grad[1];
    713                     nrm = this.f(v[0], v[1]) / denom;
    714
    715                     w[0] = v[0] - grad[0] * nrm;
    716                     w[1] = v[1] - grad[1] * nrm;
    717                     if (k === 0) {
    718                         k0 = Math.abs(nrm) * Math.sqrt(denom);
    719                     } else if (k === 1) {
    720                         k1 = Math.abs(nrm) * Math.sqrt(denom);
    721                     }
    722
    723                     v[0] = w[0];
    724                     v[1] = w[1];
    725                     k++;
    726                 } while (k < 20 &&
    727                     Math.abs(this.f(v[0], v[1])) > this.config.tol_newton
    728                 );
    729
    730                 delta = k0;
    731                 if (k > 1) {
    732                     kappa = k1 / k0;
    733                 } else {
    734                     kappa = 0.0;
    735                 }
    736
    737                 if (quasi) {
    738                     A = this.updateA(A, v_start, v);
    739                     t_v = this.tangent_A(A);
    740                 } else {
    741                     t_v = this.tangent(v);
    742                 }
    743
    744                 dir = Mat.innerProduct(t_u, t_v, 2);
    745                 dir = Math.max(-1, Math.min(1, dir));
    746                 alpha = Math.acos(dir);
    747
    748                 // Look for simple bifurcation points and cusps
    749                 cusp_or_bifurc = false;
    750                 progress = Geometry.distance(u, v, 2);
    751                 if (progress < this.config.tol_progress) {
    752                     u = v.slice();
    753                     pathX.push(u[0]);
    754                     pathY.push(u[1]);
    755                     // console.log("-> Bail out, no progress", progress, steps);
    756                     break;
    757
    758                 } else if (dir < 0.0) {
    759                     if (h > this.config.h_critical) {
    760                         // console.log("Critical point at [", u[0].toFixed(4), u[1].toFixed(4), "], v: [", v[0].toFixed(4), v[1].toFixed(4), "], but large  h:", h);
    761
    762                     } else {
    763
    764                         cusp_or_bifurc = true;
    765                         if (this.isBifurcation(u, this.config.tol_cusp)) {
    766                             // console.log(steps, "bifurcation point between", u, "and", v, ":", dir, "h", h, "alpha", alpha);
    767                             // A = [dfx(v[0], v[1]), dfy(v[0], v[1])];
    768                             omega *= (-1);
    769                             // If there is a bifurcation point, we
    770                             // ignore the angle alpha for subsequent step length
    771                             // adaption. Because then we might be able to
    772                             // "jump over the critical point"
    773                             alpha = 0;
    774                         } else {
    775                             // Cusp or something more weird
    776                             u = v.slice();
    777                             pathX.push(u[0]);
    778                             pathY.push(u[1]);
    779                             // console.log("-> Bail out, cusp")
    780                             break;
    781                         }
    782                     }
    783                 }
    784
    785                 // Adapt stepwidth
    786                 if (!cusp_or_bifurc) {
    787                     factor = Math.max(
    788                         Math.sqrt(kappa / kappa_0),
    789                         Math.sqrt(delta / delta_0),
    790                         alpha / alpha_0
    791                     );
    792                     if (isNaN(factor)) {
    793                         factor = 1;
    794                     }
    795                     factor = Math.max(Math.min(factor, 2), 0.5);
    796                     h /= factor;
    797                     h = Math.min(this.config.h_max, h);
    798
    799                     if (factor >= 2) {
    800                         steps++;
    801                         if (steps >= 3 * max_steps) {
    802                             break;
    803                         }
    804
    805                         point_added = false;
    806                         continue;
    807                     }
    808                 }
    809
    810                 u = v.slice();
    811                 pathX.push(u[0]);
    812                 pathY.push(u[1]);
    813                 point_added = true;
    814
    815                 steps++;
    816             } while (
    817                 steps < max_steps &&
    818                 u[0] >= this.bbox[0] &&
    819                 u[1] <= this.bbox[1] &&
    820                 u[0] <= this.bbox[2] &&
    821                 u[1] >= this.bbox[3]
    822             );
    823
    824             // if (!loop_closed) {
    825             //     console.log("No loop", steps);
    826             // } else {
    827             //     console.log("Loop", steps);
    828             // }
    829
    830             return [pathX, pathY, loop_closed];
    831         },
    832
    833         /**
    834          * If both eigenvalues of the Hessian are different from zero, the critical point at u
    835          * is a simple bifurcation point.
    836          *
    837          * @param {Array} u Critical point [x, y]
    838          * @param {Number} tol Tolerance of the eigenvalues to be zero.
    839          * @returns Boolean True if the point is a simple bifurcation point.
    840          * @private
    841          */
    842         isBifurcation: function (u, tol) {
    843             // Former experiments:
    844             // If the Hessian has exactly one zero eigenvalue,
    845             // we claim that there is a cusp.
    846             // Otherwise, we decide that there is a bifurcation point.
    847             // In the latter case, if both eigenvalues are zero
    848             // this is a somewhat crude decision.
    849             //
    850             var h = Mat.eps * Mat.eps * 100,
    851                 x, y, a, b, c, d, ad,
    852                 lbda1, lbda2,
    853                 dis;
    854
    855             x = u[0];
    856             y = u[1];
    857             a = 0.5 * (this.dfx(x + h, y) - this.dfx(x - h, y)) / h;
    858             b = 0.5 * (this.dfx(x, y + h) - this.dfx(x, y - h)) / h;
    859             c = 0.5 * (this.dfy(x + h, y) - this.dfy(x - h, y)) / h;
    860             d = 0.5 * (this.dfy(x, y + h) - this.dfy(x, y - h)) / h;
    861
    862             // c = b
    863             ad = a + d;
    864             dis = ad * ad - 4 * (a * d - b * c);
    865             lbda1 = 0.5 * (ad + Math.sqrt(dis));
    866             lbda2 = 0.5 * (ad - Math.sqrt(dis));
    867
    868             // console.log(a, b, c, d)
    869             // console.log("Eigenvals u:", lbda1, lbda2, tol);
    870
    871             if (Math.abs(lbda1) > tol && Math.abs(lbda2) > tol) {
    872                 // if (lbda1 * lbda2 > 0) {
    873                 //     console.log("Seems to be isolated singularity at", u);
    874                 // }
    875                 return true;
    876             }
    877
    878             return false;
    879         },
    880
    881         /**
    882          * Search in an arc around a critical point for a further point on the curve.
    883          * Unused for the moment.
    884          *
    885          * @param {Array} u Critical point [x, y]
    886          * @param {Array} t_u Tangent at u
    887          * @param {Number} r Radius
    888          * @param {Number} omega angle
    889          * @returns {Array} Coordinates [x, y] of a new point.
    890          * @private
    891          */
    892         handleCriticalPoint: function (u, t_u, r, omega) {
    893             var a = Math.atan2(omega * t_u[1], omega * t_u[0]),
    894                 // s = a - 0.75 * Math.PI,
    895                 // e = a + 0.75 * Math.PI,
    896                 f_circ = function (t) {
    897                     var x = u[0] + r * Math.cos(t),
    898                         y = u[1] + r * Math.sin(t);
    899                     return this.f(x, y);
    900                 },
    901                 x, y, t0;
    902
    903             // t0 = Numerics.fzero(f_circ, [s, e]);
    904             t0 = Numerics.root(f_circ, a);
    905
    906             x = u[0] + r * Math.cos(t0);
    907             y = u[1] + r * Math.sin(t0);
    908             // console.log("\t", "result", x, y, "f", f(x, y));
    909
    910             return [x, y];
    911         },
    912
    913         /**
    914          * Quasi-Newton update of the Moore-Penrose inverse.
    915          * See (7.2.3) in Allgower, Georg.
    916          *
    917          * @param {Array} A
    918          * @param {Array} u0
    919          * @param {Array} u1
    920          * @returns Array
    921          * @private
    922          */
    923         updateA: function (A, u0, u1) {
    924             var s = [u1[0] - u0[0], u1[1] - u0[1]],
    925                 y = this.f(u1[0], u1[1]) - this.f(u0[0], u0[1]),
    926                 nom, denom;
    927
    928             denom = s[0] * s[0] + s[1] * s[1];
    929             nom = y - (A[0] * s[0] + A[1] * s[1]);
    930             nom /= denom;
    931             A[0] += nom * s[0];
    932             A[1] += nom * s[1];
    933
    934             return A;
    935         },
    936
    937         /**
    938          * Approximate tangent (of norm 1) with Quasi-Newton method
    939          * @param {Array} A
    940          * @returns Array
    941          * @private
    942          */
    943         tangent_A: function (A) {
    944             var t = [-A[1], A[0]],
    945                 nrm = Mat.norm(t, 2);
    946
    947             if (nrm < Mat.eps) {
    948                 // console.log("Approx. Singularity", t, "is zero", nrm);
    949             }
    950             return [t[0] / nrm, t[1] / nrm];
    951         },
    952
    953         /**
    954          * Tangent of norm 1 at point u.
    955          * @param {Array} u Point [x, y]
    956          * @returns Array
    957          * @private
    958          */
    959         tangent: function (u) {
    960             var t = [-this.dfy(u[0], u[1]), this.dfx(u[0], u[1])],
    961                 nrm = Mat.norm(t, 2);
    962
    963             if (nrm < Mat.eps * Mat.eps) {
    964                 // console.log("Singularity", t, "is zero", "at", u, ":", nrm);
    965                 return false;
    966             }
    967             return [t[0] / nrm, t[1] / nrm];
    968         }
    969     }
    970
    971 );
    972
    973 export default Mat.ImplicitPlot;
    974
    975