Newton's Method, Deflation and Multiplicity =========================================== The instructions in the code cells in this chapter require the following functions to be imported: :: from phcpy.solutions import make_solution, strsol2dict from phcpy.deflation import double_newton_step from phcpy.deflation import double_double_newton_step from phcpy.deflation import quad_double_newton_step from phcpy.deflation import double_deflate from phcpy.deflation import double_multiplicity Newton's method --------------- Let us start with a simple system of two polynomials in two unknowns. :: pols = ['x^2 + 4*y^2 - 4;', '2*y^2 - x;'] At a regular solution, Newton's method doubles the accuracy in each step. For the example, we start at a solution where the coordinates are given with only three decimal places. :: sols = [make_solution(['x', 'y'], [1.23, -0.786])] print(sols[0]) Here is then the solution with three decimal places of accuracy: :: t : 0.000000000000000E+00 0.000000000000000E+00 m : 1 the solution for t : x : 1.230000000000000E+00 0.0 y : -7.860000000000000E-01 0.0 == err : 0.000E+00 = rco : 1.000E+00 = res : 0.000E+00 = The function ``double_newton_step`` expects a list of solution strings as its second argument. :: sols = double_newton_step(pols, sols) print(sols[0]) The outcome of one step with Newton's method in double precision is :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 0 the solution for t : x : 1.23607623318386E+00 0.00000000000000E+00 y : -7.86154018188250E-01 0.00000000000000E+00 == err : 6.230E-03 = rco : 1.998E-01 = res : 3.706E-05 = Observe the values for ``err`` (forward error), ``rco`` (estimated inverse of the condition number), and ``res`` (the residual or backward error). The multiplicity field ``m`` turned ``0`` because the default tolerance was not reached and the solution could not be counted as a proper solution. Let us reset the multiplicity, making a new solution with the values from the ``sols[0]``. The string representation of the solution is converted into a dictionary first: :: sold = strsol2dict(sols[0]) sold which yields :: {'t': 0j, 'm': 0, 'err': 0.00623, 'rco': 0.1998, 'res': 3.706e-05, 'x': (1.23607623318386+0j), 'y': (-0.78615401818825+0j)} and then we make a new solution: :: sol = make_solution(['x', 'y'], [sold['x'], sold['y']]) print(sol) As input for the next Newton step we will use :: t : 0.000000000000000E+00 0.000000000000000E+00 m : 1 the solution for t : x : 1.236076233183860E+00 0.000000000000000E+00 y : -7.861540181882500E-01 0.000000000000000E+00 == err : 0.000E+00 = rco : 1.000E+00 = res : 0.000E+00 = Calling ``double_newton_step`` for the second time :: sols = [sol] sols = double_newton_step(pols, sols) print(sols[0]) gives :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 0 the solution for t : x : 1.23606797751503E+00 0.00000000000000E+00 y : -7.86151377766704E-01 0.00000000000000E+00 == err : 1.090E-05 = rco : 1.998E-01 = res : 1.100E-10 = Observe that the value of ``res`` dropped from magnitude ``1.0e-5`` down to ``1.0e-10``, corresponding to the well conditioning of the root. However, the multiplicy field is still zero because the estimate for the forward error is still too high. The third application of the Newton step :: sold = strsol2dict(sols[0]) sol = make_solution(['x', 'y'], [sold['x'], sold['y']]) sols = [sol] sols = double_newton_step(pols, sols) print(sols[0]) then yields the solution :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 1 the solution for t : x : 1.23606797749979E+00 0.00000000000000E+00 y : -7.86151377757423E-01 0.00000000000000E+00 == err : 2.452E-11 = rco : 1.998E-01 = res : 4.441E-16 = The value for the residual ``res`` is very close to machine precision and the solution is considered a proper regular solution. We can double the precision to double double, and continue with the previous approximate solution: :: sols = double_double_newton_step(pols, sols) print(sols[0]) what is printed is below :: t : 0.00000000000000000000000000000000E+00 0.00000000000000000000000000000000E+00 m : 1 the solution for t : x : 1.23606797749978969640917366873130E+00 0.00000000000000000000000000000000E+00 y : -7.86151377757423286069558585843026E-01 0.00000000000000000000000000000000E+00 == err : 3.036E-16 = rco : 1.998E-01 = res : 3.944E-31 = and doing this once more :: sols = double_double_newton_step(pols, sols)\n", print(sols[0])" shows :: t : 0.00000000000000000000000000000000E+00 0.00000000000000000000000000000000E+00 m : 1 the solution for t : x : 1.23606797749978969640917366873128E+00 0.00000000000000000000000000000000E+00 y : -7.86151377757423286069558585842966E-01 0.00000000000000000000000000000000E+00 == err : 6.272E-32 = rco : 1.998E-01 = res : 0.000E+00 = and then on to quad double precision: :: sols = quad_double_newton_step(pols, sols) print(sols[0]) which then gives the solution: :: t : 0.0000000000000000000000000000000000000000000000000000000000000000E+00 0.0000000000000000000000000000000000000000000000000000000000000000E+00 m : 1 the solution for t : x : 1.2360679774997896964091736687312762354406183596115257242708972454E+00 0.0000000000000000000000000000000000000000000000000000000000000000E+00 y : -7.8615137775742328606955858584295892952312205783772323766490197015E-01 0.0000000000000000000000000000000000000000000000000000000000000000E+00 == err : 7.070E-33 = rco : 1.998E-01 = res : 2.101E-64 = deflation --------- At an isolated singular solution, *deflation* is a method to restore the quadratic convergence of Newton's method. :: pols = ['(29/16)*x^3 - 2*x*y;', 'x^2 - y;'] Obviously, ``(0, 0)`` is a root of the ``pols`` but even if we start very close to ``(0, 0)``, Newton's method converges extremely slowly. :: sols = [make_solution(['x', 'y'],[1.0e-6, 1.0e-6])] print(sols[0]) The initial solution is then :: t : 0.000000000000000E+00 0.000000000000000E+00 m : 1 the solution for t : x : 1.000000000000000E-06 0.0 y : 1.000000000000000E-06 0.0 == err : 0.000E+00 = rco : 1.000E+00 = res : 0.000E+00 = Running one step with Newton's method: :: sols = double_newton_step(pols, sols) print(sols[0]) gives :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 0 the solution for t : x : 9.99999906191101E-07 0.00000000000000E+00 y : 9.99999812409806E-13 0.00000000000000E+00 == err : 1.000E-06 = rco : 5.625E-13 = res : 1.875E-19 = Observe the tiny value for ``rco``, the estimate of the inverse of the condition number. The next step with Newton's method :: sols = double_newton_step(pols, sols) print(sols[0]) shows :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 0 the solution for t :\n", x : 6.66666604160106E-07 0.00000000000000E+00 y : 3.33333270859482E-13 0.00000000000000E+00 == err : 3.333E-07 = rco : 2.778E-14 = res : 1.111E-13 = which does not brings us much closer to ``(0, 0)``. Now we apply deflation in double precison: :: solsd = double_deflate(pols, sols) print(solsd[0]) which then yields :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 1 the solution for t : x : 9.46532112069346E-24 4.09228221015004E-24 y : 1.02357542351685E-24 -2.03442589046821E-24 == err : 5.292E-12 = rco : 5.608E-03 = res : 1.885E-15 = Deflation also works on systems with more equations than unknowns. :: pols = ['x^2;', 'x*y;', 'y^2;'] Once again, ``(0, 0)`` is the obvious root of ``pols``. :: sols = [make_solution(['x', 'y'], [1.0e-6, 1.0e-6])] print(sols[0] So, we start at :: t : 0.000000000000000E+00 0.000000000000000E+00 m : 1 the solution for t : x : 1.000000000000000E-06 0.0 y : 1.000000000000000E-06 0.0 == err : 0.000E+00 = rco : 1.000E+00 = res : 0.000E+00 = and apply deflation :: sols = double_deflate(pols, sols, tolrnk=1.0e-8) print(sols[0]) which shows :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 1 the solution for t : x : 1.25000000000000E-07 0.00000000000000E+00 y : 1.25000000000000E-07 0.00000000000000E+00 == err : 1.250E-07 = rco : 8.165E-01 = res : 1.562E-14 = This is not an improvement, because the tolerance on the numerical rank, given by ``tolrnk=1.0e-8`` is too severe. If we relax this tolerance to ``1.0e-4``, :: sols = double_deflate(pols, sols, tolrnk=1.0e-4) print(sols[0]) then we obtain :: t : 0.00000000000000E+00 0.00000000000000E+00 m : 1 the solution for t : x : -5.87747175411144E-39 -5.14278778484751E-39 y : 2.93873587705572E-39 -1.83670992315982E-40 == err : 2.757E-23 = rco : 4.082E-01 = res : 1.984E-38 = and the coordinates of this numerical approximation are well below the double precision. multiplicity structure ---------------------- The multiplicity can be computed *locally* starting at the solution. Consider the following example: :: pols = [ 'x**2+y-3;', 'x+0.125*y**2-1.5;'] The solution ``(1, 2)`` is a multiple root of ``pols``. Let us prepare the input: :: sol = make_solution(['x', 'y'], [1, 2]) print(sol) so, we work with :: t : 0.000000000000000E+00 0.000000000000000E+00 m : 1 the solution for t : x : 1.000000000000000E+00 0.0 y : 2.000000000000000E+00 0.0 == err : 0.000E+00 = rco : 1.000E+00 = res : 0.000E+00 = as input to ``double_multiplicity`` :: multiplicity, hilbert_function = double_multiplicity(pols, sol) The value of ``multiplicity`` equals ``3`` which is confirmed by the content of ``hilbert_function``: :: [1, 1, 1, 0, 0, 0] Thus, the multiplicity of ``(1, 2)`` as a root of ``pols`` equals three.