1 /* 2 Copyright 2008-2024 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); 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); 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 * @returns {Array} consisting of [dataX, dataY, number_of_components]- 318 * @private 319 */ 320 searchLine: function (fmi, fma, fix, interval, dir, 321 num_components, dataX, dataY) { 322 var t_mi, t_ma, t, 323 ft, 324 mi, ma, tmp, m, 325 is_in, 326 u0, i, le, 327 ret, 328 offset, 329 delta, 330 eps = this.config.tol_u0, 331 DEBUG = false, 332 b = interval[0], 333 e = interval[1]; 334 335 t_mi = Numerics.fminbr(fmi, [b, e]); 336 mi = fmi(t_mi); 337 t_ma = Numerics.fminbr(fma, [b, e]); 338 ma = fmi(t_ma); 339 340 if (mi < eps && ma > -eps) { 341 tmp = t_mi; 342 t_mi = Math.min(tmp, t_ma); 343 t_ma = Math.max(tmp, t_ma); 344 345 t = Numerics.fzero(fmi, [t_mi, t_ma]); 346 // t = Numerics.chandrupatla(fmi, [t_mi, t_ma]); 347 348 ft = fmi(t); 349 if (Math.abs(ft) > Math.max((ma - mi) * Mat.eps, 0.001)) { 350 //console.log("searchLine:", dir, fix, t, "no root " + ft); 351 return false; 352 // throw new Error("searchLine: no root " + ft); 353 } 354 if (dir === 'vertical') { 355 u0 = [1, fix, t]; 356 delta = this.config.resolution_in / this.config.unitY; 357 // console.log("Inner delta x", delta) 358 } else { 359 u0 = [1, t, fix]; 360 delta = this.config.resolution_in / this.config.unitX; 361 // console.log("Inner delta y", delta) 362 } 363 delta *= (1 + Mat.eps); 364 365 is_in = this.curveContainsPoint(u0, dataX, dataY, 366 delta * 2, // Allowed dist from segment 367 this.config.qdt_box // 0.5 of box size to search in qdt 368 ); 369 370 if (is_in) { 371 if (DEBUG) { 372 console.log("Found in quadtree", u0); 373 } 374 } else { 375 if (DEBUG) { 376 console.log("Not in quadtree", u0, dataX.length); 377 } 378 ret = this.traceComponent(u0, 1); 379 if (ret.length > 0) { 380 // Add jump in curve 381 if (num_components > 0) { 382 dataX.push(NaN); 383 dataY.push(NaN); 384 } 385 386 offset = dataX.length; 387 le = ret[0].length; 388 for (i = 1; i < le; i++) { 389 this.qdt.insertItem({ 390 xlb: Math.min(ret[0][i - 1], ret[0][i]), 391 xub: Math.max(ret[0][i - 1], ret[0][i]), 392 ylb: Math.min(ret[1][i - 1], ret[1][i]), 393 yub: Math.max(ret[1][i - 1], ret[1][i]), 394 idx1: offset + i - 1, 395 idx2: offset + i, 396 comp: num_components 397 }); 398 } 399 400 num_components++; 401 Type.concat(dataX, ret[0]); 402 Type.concat(dataY, ret[1]); 403 } 404 } 405 406 m = t - delta * 0.01; 407 if (m - b > delta) { 408 ret = this.searchLine( 409 fmi, fma, fix, [b, m], dir, 410 num_components, dataX, dataY); 411 if (ret !== false) { 412 dataX = ret[0]; 413 dataY = ret[1]; 414 num_components = ret[2]; 415 } 416 } 417 m = t + delta * 0.01; 418 if (e - m > delta) { 419 ret = this.searchLine( 420 fmi, fma, fix, [m, e], dir, 421 num_components, dataX, dataY); 422 if (ret !== false) { 423 dataX = ret[0]; 424 dataY = ret[1]; 425 num_components = ret[2]; 426 } 427 } 428 429 return [dataX, dataY, num_components]; 430 } 431 432 return false; 433 }, 434 435 /** 436 * Test if the data points contain a given coordinate, i.e. if the 437 * given coordinate is close enough to the polygonal chain 438 * through the data points. 439 * 440 * @param {Array} p Homogenous coordinates [1, x, y] of the coordinate point 441 * @param {Array} dataX x-coordinates of points so far 442 * @param {Array} dataY y-coordinates of points so far 443 * @param {Number} tol Maximal distance of p from the polygonal chain through the data points 444 * @param {Number} eps Helper tolerance used for the quadtree 445 * @returns Boolean 446 */ 447 curveContainsPoint: function (p, dataX, dataY, tol, eps) { 448 var i, le, hits, d, 449 x = p[1], 450 y = p[2]; 451 452 hits = this.qdt.find([x - eps, y + eps, x + eps, y - eps]); 453 454 le = hits.length; 455 for (i = 0; i < le; i++) { 456 d = Geometry.distPointSegment( 457 p, 458 [1, dataX[hits[i].idx1], dataY[hits[i].idx1]], 459 [1, dataX[hits[i].idx2], dataY[hits[i].idx2]] 460 ); 461 if (d < tol) { 462 return true; 463 } 464 } 465 return false; 466 }, 467 468 /** 469 * Starting at an initial point the curve is traced with a Euler-Newton method. 470 * After tracing in one direction the algorithm stops if the component is a closed loop. 471 * Otherwise, the curved is traced in the opposite direction, starting from 472 * the same initial point. Finally, the two components are glued together. 473 * 474 * @param {Array} u0 Initial point in homogenous coordinates [1, x, y]. 475 * @returns Array [dataX, dataY] containing a new component. 476 * @private 477 */ 478 traceComponent: function (u0) { 479 var dataX = [], 480 dataY = [], 481 arr = []; 482 483 // Trace in first direction 484 // console.log("---- Start tracing forward ---------") 485 arr = this.tracing(u0, 1); 486 487 if (arr.length === 0) { 488 // console.log("Could not start tracing due to singularity") 489 } else { 490 // console.log("Trace from", [arr[0][0], arr[1][0]], "to", [arr[0][arr[0].length - 1], arr[1][arr[1].length - 1]], 491 // "num points:", arr[0].length); 492 dataX = arr[0]; 493 dataY = arr[1]; 494 } 495 496 // Trace in the other direction 497 if (!arr[2]) { 498 // No loop in the first tracing step, 499 // now explore the other direction. 500 501 // console.log("---- Start tracing backward ---------") 502 arr = this.tracing(u0, -1); 503 504 if (arr.length === 0) { 505 // console.log("Could not start backward tracing due to singularity") 506 } else { 507 // console.log("Trace backwards from", [arr[0][0], arr[1][0]], "to", 508 // [arr[0][arr[0].length - 1], arr[1][arr[1].length - 1]], "num points:", arr[0].length); 509 dataX = arr[0].reverse().concat(dataX.slice(1)); 510 dataY = arr[1].reverse().concat(dataY.slice(1)); 511 } 512 } 513 514 if (dataX.length < 6) { 515 // Solitary point 516 dataX.push(dataX[dataX.length - 1]); 517 dataY.push(dataY[dataY.length - 1]); 518 } 519 520 return [dataX, dataY]; 521 }, 522 523 /** 524 * Starting at a point <i>u0</i>, this routine traces the curve <i>f(u)=0</i> until 525 * a loop is detected, a critical point is reached, the curve leaves the bounding box, 526 * or the maximum number of points is reached. 527 * <p> 528 * The method is a predictor / corrector method consisting of Euler and Newton steps 529 * together with step width adaption. 530 * <p> 531 * The algorithm is an adaption of the algorithm in 532 * Eugene L. Allgower, Kurt Georg: <i>Introduction to Numerical Continuation methods.</i> 533 * 534 * @param {Array} u0 Starting point in homogenous coordinates [1, x, y]. 535 * @param {Number} direction 1 or -1 536 * @returns Array [pathX, pathY, loop_closed] or [] 537 * @private 538 */ 539 tracing: function (u0, direction) { 540 var u = [], 541 v = [], 542 v_start = [], 543 w = [], 544 t_u, t_v, t_u_0, 545 A, 546 grad, 547 nrm, 548 dir, 549 steps = 0, 550 k = 0, 551 loop_closed = false, 552 k0, k1, denom, dist, progress, 553 kappa, delta, alpha, 554 factor, 555 point_added = false, 556 557 quasi = false, 558 cusp_or_bifurc = false, 559 kappa_0 = this.config.kappa_0, // Inverse of planned number of Newton steps 560 delta_0 = this.config.delta_0, // Distance of predictor point to curve 561 alpha_0 = this.config.alpha_0, // Angle between two successive tangents 562 h = this.config.h_initial, 563 max_steps = this.config.max_steps, 564 565 omega = direction, 566 pathX = [], 567 pathY = [], 568 569 T = [], // Gosper's loop detector table 570 n, m, i, e; 571 572 u = u0.slice(1); 573 pathX.push(u[0]); 574 pathY.push(u[1]); 575 576 t_u = this.tangent(u); 577 if (t_u === false) { 578 // We don't want to start at a singularity. 579 // Get out of here and search for another starting point. 580 return []; 581 } 582 A = [this.dfx(u[0], u[1]), this.dfy(u[0], u[1])]; 583 584 do { 585 586 if (quasi) { 587 t_u = this.tangent_A(A); 588 } else { 589 t_u = this.tangent(u); 590 } 591 if (t_u === false) { 592 u = v.slice(); 593 pathX.push(u[0]); 594 pathY.push(u[1]); 595 // console.log("-> Bail out: t_u undefined."); 596 break; 597 } 598 599 if (pathX.length === 1) { 600 // Store first point 601 t_u_0 = t_u.slice(); 602 } else if (pathX.length === 2) { 603 T.push(pathX.length - 1); // Put first point into Gosper table T 604 605 } else if (point_added && pathX.length > 2 && !cusp_or_bifurc) { 606 607 // Detect if loop has been closed 608 dist = Geometry.distPointSegment( 609 [1, u[0], u[1]], 610 [1, pathX[0], pathY[0]], 611 [1, pathX[1], pathY[1]] 612 ); 613 614 if (dist < this.config.loop_dist * h && 615 Mat.innerProduct(t_u, t_u_0, 2) > this.config.loop_dir 616 ) { 617 618 // console.log("Loop detected after", steps, "steps"); 619 // console.log("\t", "v", v, "u0:", u0) 620 // console.log("\t", "Dist(v, path0)", dist, config.loop_dist * h) 621 // console.log("\t", "t_u", t_u); 622 // console.log("\t", "inner:", Mat.innerProduct(t_u, t_u_0, 2)); 623 // console.log("\t", "h", h); 624 625 u = u0.slice(1); 626 pathX.push(u[0]); 627 pathY.push(u[1]); 628 629 loop_closed = true; 630 break; 631 } 632 633 // Gosper's loop detector 634 if (this.config.loop_detection) { 635 n = pathX.length - 1; 636 // console.log("Check Gosper", n); 637 m = Math.floor(Mat.log2(n)); 638 639 for (i = 0; i <= m; i++) { 640 dist = Geometry.distPointSegment( 641 [1, u[0], u[1]], 642 [1, pathX[T[i] - 1], pathY[T[i] - 1]], 643 [1, pathX[T[i]], pathY[T[i]]] 644 ); 645 646 if (dist < this.config.loop_dist * h) { 647 // console.log("!!!!!!!!!!!!!!! GOSPER LOOP CLOSED !!!!", i, n + 1, 648 // this.config.loop_dist * h 649 // ); 650 651 t_v = this.tangent([pathX[T[i]], pathY[T[i]]]); 652 if (Mat.innerProduct(t_u, t_v, 2) > this.config.loop_dir) { 653 // console.log("!!!!!!!!!!!!!!! angle is good enough"); 654 break; 655 } 656 } 657 } 658 if (i <= m) { 659 loop_closed = true; 660 break; 661 } 662 663 m = 1; 664 e = 0; 665 for (i = 0; i < 100; i++) { 666 if ((n + 1) % m !== 0) { 667 break; 668 } 669 m *= 2; 670 e++; 671 } 672 // console.log("Add at e", e); 673 T[e] = n; 674 } 675 676 } 677 678 // Predictor step 679 // if (true /*h < 2 * this.config.h_initial*/) { 680 // Euler 681 // console.log("euler") 682 v[0] = u[0] + h * omega * t_u[0]; 683 v[1] = u[1] + h * omega * t_u[1]; 684 // } else { 685 // // Heun 686 // // console.log("heun") 687 // v[0] = u[0] + h * omega * t_u[0]; 688 // v[1] = u[1] + h * omega * t_u[1]; 689 690 // t_v = this.tangent(v); 691 // v[0] = 0.5 * u[0] + 0.5 * (v[0] + h * omega * t_v[0]); 692 // v[1] = 0.5 * u[1] + 0.5 * (v[1] + h * omega * t_v[1]); 693 // } 694 if (quasi) { 695 A = this.updateA(A, u, v); 696 v_start = v.slice(); 697 } 698 699 // Corrector step: Newton 700 k = 0; 701 do { 702 if (quasi) { 703 grad = A; 704 } else { 705 grad = [this.dfx(v[0], v[1]), this.dfy(v[0], v[1])]; 706 } 707 708 // Compute w = v - A(v) * f(v), 709 // grad: row vector and A(v) is the Moore-Penrose inverse: 710 // grad^T * (grad * grad^T)^(-1) 711 denom = grad[0] * grad[0] + grad[1] * grad[1]; 712 nrm = this.f(v[0], v[1]) / denom; 713 714 w[0] = v[0] - grad[0] * nrm; 715 w[1] = v[1] - grad[1] * nrm; 716 if (k === 0) { 717 k0 = Math.abs(nrm) * Math.sqrt(denom); 718 } else if (k === 1) { 719 k1 = Math.abs(nrm) * Math.sqrt(denom); 720 } 721 722 v[0] = w[0]; 723 v[1] = w[1]; 724 k++; 725 } while (k < 20 && 726 Math.abs(this.f(v[0], v[1])) > this.config.tol_newton 727 ); 728 729 delta = k0; 730 if (k > 1) { 731 kappa = k1 / k0; 732 } else { 733 kappa = 0.0; 734 } 735 736 if (quasi) { 737 A = this.updateA(A, v_start, v); 738 t_v = this.tangent_A(A); 739 } else { 740 t_v = this.tangent(v); 741 } 742 743 dir = Mat.innerProduct(t_u, t_v, 2); 744 dir = Math.max(-1, Math.min(1, dir)); 745 alpha = Math.acos(dir); 746 747 // Look for simple bifurcation points and cusps 748 cusp_or_bifurc = false; 749 progress = Geometry.distance(u, v, 2); 750 if (progress < this.config.tol_progress) { 751 u = v.slice(); 752 pathX.push(u[0]); 753 pathY.push(u[1]); 754 // console.log("-> Bail out, no progress", progress, steps); 755 break; 756 757 } else if (dir < 0.0) { 758 if (h > this.config.h_critical) { 759 // 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); 760 761 } else { 762 763 cusp_or_bifurc = true; 764 if (this.isBifurcation(u, this.config.tol_cusp)) { 765 // console.log(steps, "bifurcation point between", u, "and", v, ":", dir, "h", h, "alpha", alpha); 766 // A = [dfx(v[0], v[1]), dfy(v[0], v[1])]; 767 omega *= (-1); 768 // If there is a bifurcation point, we 769 // ignore the angle alpha for subsequent step length 770 // adaption. Because then we might be able to 771 // "jump over the critical point" 772 alpha = 0; 773 } else { 774 // Cusp or something more weird 775 u = v.slice(); 776 pathX.push(u[0]); 777 pathY.push(u[1]); 778 // console.log("-> Bail out, cusp") 779 break; 780 } 781 } 782 } 783 784 // Adapt stepwidth 785 if (!cusp_or_bifurc) { 786 factor = Math.max( 787 Math.sqrt(kappa / kappa_0), 788 Math.sqrt(delta / delta_0), 789 alpha / alpha_0 790 ); 791 if (isNaN(factor)) { 792 factor = 1; 793 } 794 factor = Math.max(Math.min(factor, 2), 0.5); 795 h /= factor; 796 h = Math.min(this.config.h_max, h); 797 798 if (factor >= 2) { 799 steps++; 800 if (steps >= 3 * max_steps) { 801 break; 802 } 803 804 point_added = false; 805 continue; 806 } 807 } 808 809 u = v.slice(); 810 pathX.push(u[0]); 811 pathY.push(u[1]); 812 point_added = true; 813 814 steps++; 815 } while ( 816 steps < max_steps && 817 u[0] >= this.bbox[0] && 818 u[1] <= this.bbox[1] && 819 u[0] <= this.bbox[2] && 820 u[1] >= this.bbox[3] 821 ); 822 823 // if (!loop_closed) { 824 // console.log("No loop", steps); 825 // } else { 826 // console.log("Loop", steps); 827 // } 828 829 return [pathX, pathY, loop_closed]; 830 }, 831 832 /** 833 * If both eigenvalues of the Hessian are different from zero, the critical point at u 834 * is a simple bifurcation point. 835 * 836 * @param {Array} u Critical point [x, y] 837 * @param {Number} tol Tolerance of the eigenvalues to be zero. 838 * @returns Boolean True if the point is a simple bifurcation point. 839 * @private 840 */ 841 isBifurcation: function (u, tol) { 842 // Former experiments: 843 // If the Hessian has exactly one zero eigenvalue, 844 // we claim that there is a cusp. 845 // Otherwise, we decide that there is a bifurcation point. 846 // In the latter case, if both eigenvalues are zero 847 // this is a somewhat crude decision. 848 // 849 var h = Mat.eps * Mat.eps * 100, 850 x, y, a, b, c, d, ad, 851 lbda1, lbda2, 852 dis; 853 854 x = u[0]; 855 y = u[1]; 856 a = 0.5 * (this.dfx(x + h, y) - this.dfx(x - h, y)) / h; 857 b = 0.5 * (this.dfx(x, y + h) - this.dfx(x, y - h)) / h; 858 c = 0.5 * (this.dfy(x + h, y) - this.dfy(x - h, y)) / h; 859 d = 0.5 * (this.dfy(x, y + h) - this.dfy(x, y - h)) / h; 860 861 // c = b 862 ad = a + d; 863 dis = ad * ad - 4 * (a * d - b * c); 864 lbda1 = 0.5 * (ad + Math.sqrt(dis)); 865 lbda2 = 0.5 * (ad - Math.sqrt(dis)); 866 867 // console.log(a, b, c, d) 868 // console.log("Eigenvals u:", lbda1, lbda2, tol); 869 870 if (Math.abs(lbda1) > tol && Math.abs(lbda2) > tol) { 871 // if (lbda1 * lbda2 > 0) { 872 // console.log("Seems to be isolated singularity at", u); 873 // } 874 return true; 875 } 876 877 return false; 878 }, 879 880 /** 881 * Search in an arc around a critical point for a further point on the curve. 882 * Unused for the moment. 883 * 884 * @param {Array} u Critical point [x, y] 885 * @param {Array} t_u Tangent at u 886 * @param {Number} r Radius 887 * @param {Number} omega angle 888 * @returns {Array} Coordinates [x, y] of a new point. 889 * @private 890 */ 891 handleCriticalPoint: function (u, t_u, r, omega) { 892 var a = Math.atan2(omega * t_u[1], omega * t_u[0]), 893 // s = a - 0.75 * Math.PI, 894 // e = a + 0.75 * Math.PI, 895 f_circ = function (t) { 896 var x = u[0] + r * Math.cos(t), 897 y = u[1] + r * Math.sin(t); 898 return this.f(x, y); 899 }, 900 x, y, t0; 901 902 // t0 = Numerics.fzero(f_circ, [s, e]); 903 t0 = Numerics.root(f_circ, a); 904 905 x = u[0] + r * Math.cos(t0); 906 y = u[1] + r * Math.sin(t0); 907 // console.log("\t", "result", x, y, "f", f(x, y)); 908 909 return [x, y]; 910 }, 911 912 /** 913 * Quasi-Newton update of the Moore-Penrose inverse. 914 * See (7.2.3) in Allgower, Georg. 915 * 916 * @param {Array} A 917 * @param {Array} u0 918 * @param {Array} u1 919 * @returns Array 920 * @private 921 */ 922 updateA: function (A, u0, u1) { 923 var s = [u1[0] - u0[0], u1[1] - u0[1]], 924 y = this.f(u1[0], u1[1]) - this.f(u0[0], u0[1]), 925 nom, denom; 926 927 denom = s[0] * s[0] + s[1] * s[1]; 928 nom = y - (A[0] * s[0] + A[1] * s[1]); 929 nom /= denom; 930 A[0] += nom * s[0]; 931 A[1] += nom * s[1]; 932 933 return A; 934 }, 935 936 /** 937 * Approximate tangent (of norm 1) with Quasi-Newton method 938 * @param {Array} A 939 * @returns Array 940 * @private 941 */ 942 tangent_A: function (A) { 943 var t = [-A[1], A[0]], 944 nrm = Mat.norm(t, 2); 945 946 if (nrm < Mat.eps) { 947 // console.log("Approx. Singularity", t, "is zero", nrm); 948 } 949 return [t[0] / nrm, t[1] / nrm]; 950 }, 951 952 /** 953 * Tangent of norm 1 at point u. 954 * @param {Array} u Point [x, y] 955 * @returns Array 956 * @private 957 */ 958 tangent: function (u) { 959 var t = [-this.dfy(u[0], u[1]), this.dfx(u[0], u[1])], 960 nrm = Mat.norm(t, 2); 961 962 if (nrm < Mat.eps * Mat.eps) { 963 // console.log("Singularity", t, "is zero", "at", u, ":", nrm); 964 return false; 965 } 966 return [t[0] / nrm, t[1] / nrm]; 967 } 968 } 969 970 ); 971 972 export default Mat.ImplicitPlot; 973 974