symja_android_library icon indicating copy to clipboard operation
symja_android_library copied to clipboard

Small values in equation are solved to zero

Open HannesWell opened this issue 3 years ago • 5 comments

Describe the bug

When solving an equation were either side computes to a value below a certain threshold the solution becomes zero, although the value is non zero if the corresponding side is evaluated directly.

I already tried to evaluate with an increased numericPrecision (using EvalEngine.setNumericPrecision()) but this only increased the runtime and did not improve the result. Nevertheless I would expect the numericPrecision to influence the number of precise significant digits of the solution (regardless of the magnitude) and not the minimal absolute value that is computed correctly. It shouldn't matter if my value is 3.6*10^-1 or 3.6*10^-100 as long as the number of significant digits is low.

To Reproduce Example Symja Java Code: term3 is the original expression, while term2 and term1 are simplified versions that show the same behavior.

    EvalEngine e = new EvalEngine(true);
    System.out.println("numericMode: " + e.isNumericMode());
    System.out.println("numericPrecision: " + e.getNumericPrecision());

    String term1 = "1.29600*10^-17";
    String term2 = "(3.6*10^-9)^2";
    String term3 = "(1 / (4 * pi * 3 * 10^6 * 2.2 * 10^9))^2";

    IExpr power = e.parse(term1);
    IExpr solve = e.parse("Solve(x==" + term1 + ",x)");

    System.out.println(e.evaluateNIL(solve));
    System.out.println(e.evaluateNIL(power));

Error print-out:

numericMode: false
numericPrecision: 15
{{x->0}}
1.45376*10^-34
respectively 1.29600*10^-17

Expected behavior

It is expected that the equation is solved to the same value as if the term was evaluated directly. Expected (correct) result expression:

{{x->1.29600*10^-17}}
1.29600*10^-17

Java

  • Version: Java 11
  • OS: Windows 10

HannesWell avatar Jan 28 '22 18:01 HannesWell

Because the Solve function at first tries a "symbolic approach", it rationalizes all Java double values to IRational instances. Only if the symbolic evaluation doesn't succeed a FindRoot function is called.

If you knew you have only univariate numeric inputs, you can call FindRoot directly for these equations.

With the new commit which introduces the new parameter Config.ZERO_IN_OUTPUT_FORMAT which parameterizes if the double value should be printed as 0.0 in the output and the Config.DOUBLE_TOLERANCE which parameterizes if a numeric value is considered as 0 in the isZero() methods.

      Config.ZERO_IN_OUTPUT_FORMAT = 1.0e-100;
      Config.DOUBLE_TOLERANCE = 1.0e-100;

      EvalEngine e = new EvalEngine(true);
      System.out.println("numericMode: " + e.isNumericMode());
      System.out.println("numericPrecision: " + e.getNumericPrecision());

      String term1 = "1.29600*10^-17";
      String term2 = "(3.6*10^-9)^2";
      String term3 = "(1 / (4 * pi * 3 * 10^6 * 2.2 * 10^9))^2";

      IExpr power = e.parse(term1); 

      IExpr solve = e.parse("FindRoot(x-" + term1 + ",  {x,1})");
      IExpr evaledSolve = e.evaluate(solve);
      System.out.println("evaledSolve 1: " + evaledSolve);
      IExpr evaledPower = e.evaluate(power);
      System.out.println("evaledPower: " + evaledPower);

      solve = e.parse("Solve(x==-" + term1 + ", x)");
      evaledSolve = e.evaluate(solve);
      System.out.println("evaledSolve 2: " + evaledSolve);

axkr avatar Jan 28 '22 23:01 axkr

Can you try the variants with these settings?

      Config.ZERO_IN_OUTPUT_FORMAT = 1.0e-100;
      Config.DOUBLE_TOLERANCE = 1.0e-100;
      Config.DEFAULT_ROOTS_CHOP_DELTA = 1.0e-100;
      EvalEngine e = new EvalEngine(true);
      System.out.println("numericMode: " + e.isNumericMode());
      System.out.println("numericPrecision: " + e.getNumericPrecision());

      String term1 = "1.29600*10^-17";
      String term2 = "(3.6*10^-9)^2";
      String term3 = "(1 / (4 * pi * 3 * 10^6 * 2.2 * 10^9))^2";

      IExpr power = e.parse(term1);
      // IExpr solve = e.parse("Solve(x==" + term1 + ",x)");
      IExpr solve = e.parse("FindRoot(x-" + term1 + ",  {x,1})");
      IExpr evaledSolve = e.evaluate(solve);
      System.out.println("evaledRoot 1: " + evaledSolve);
      IExpr evaledPower = e.evaluate(power);
      System.out.println("evaledPower: " + evaledPower);

      solve = e.parse("Solve(x==" + term1 + ", x)");
      evaledSolve = e.evaluate(solve);
      System.out.println("evaledSolve 2: " + evaledSolve);

      IExpr nsolve = e.parse("NSolve(x==" + term1 + ", x)");
      IExpr evaledNSolve = e.evaluate(nsolve);
      System.out.println("evaledNSolve 3: " + evaledNSolve);

axkr avatar Jan 30 '22 13:01 axkr

Because the Solve function at first tries a "symbolic approach", it rationalizes all Java double values to IRational instances. Only if the symbolic evaluation doesn't succeed a FindRoot function is called.

If you knew you have only univariate numeric inputs, you can call FindRoot directly for these equations.

Would it be possible respectively reasonable to not rationalize double values and to perform all symbolic transformations using doubles? The parser still can detect integer values or fractions as such. Or could this have an negative effect on other calculations.

Alternatively if rationalization has to be done, wouldn't it be better to rationalize mantissa and exponent of a double independently? For example 1.29600*10^-17 could be split into its mantissa 1.29600 and its exponent 10^-17. The mantissa would then be rationalized to 1296/1000 and 10^-17 stays as it is, so that the result is 1296/1000 * 10^-17?

With the new commit which introduces the new parameter Config.ZERO_IN_OUTPUT_FORMAT which parameterizes if the double value should be printed as 0.0 in the output and the Config.DOUBLE_TOLERANCE which parameterizes if a numeric value is considered as 0 in the isZero() methods.

      Config.ZERO_IN_OUTPUT_FORMAT = 1.0e-100;
      Config.DOUBLE_TOLERANCE = 1.0e-100;

      EvalEngine e = new EvalEngine(true);
      System.out.println("numericMode: " + e.isNumericMode());
      System.out.println("numericPrecision: " + e.getNumericPrecision());

      String term1 = "1.29600*10^-17";
      String term2 = "(3.6*10^-9)^2";
      String term3 = "(1 / (4 * pi * 3 * 10^6 * 2.2 * 10^9))^2";

      IExpr power = e.parse(term1); 

      IExpr solve = e.parse("FindRoot(x-" + term1 + ",  {x,1})");
      IExpr evaledSolve = e.evaluate(solve);
      System.out.println("evaledSolve 1: " + evaledSolve);
      IExpr evaledPower = e.evaluate(power);
      System.out.println("evaledPower: " + evaledPower);

      solve = e.parse("Solve(x==-" + term1 + ", x)");
      evaledSolve = e.evaluate(solve);
      System.out.println("evaledSolve 2: " + evaledSolve);

This gives me the following output with the current master (17757be70fe062331669e17a1729d936c6c19d0c):

numericMode: false
numericPrecision: 15
evaledSolve 1: {x->1.2960000000000002E-17}
evaledPower: 1.29600*10^-17
evaledSolve 2: {{x->0.0}}

Can you try the variants with these settings?

      Config.ZERO_IN_OUTPUT_FORMAT = 1.0e-100;
      Config.DOUBLE_TOLERANCE = 1.0e-100;
      Config.DEFAULT_ROOTS_CHOP_DELTA = 1.0e-100;
      EvalEngine e = new EvalEngine(true);
      System.out.println("numericMode: " + e.isNumericMode());
      System.out.println("numericPrecision: " + e.getNumericPrecision());

      String term1 = "1.29600*10^-17";
      String term2 = "(3.6*10^-9)^2";
      String term3 = "(1 / (4 * pi * 3 * 10^6 * 2.2 * 10^9))^2";

      IExpr power = e.parse(term1);
      // IExpr solve = e.parse("Solve(x==" + term1 + ",x)");
      IExpr solve = e.parse("FindRoot(x-" + term1 + ",  {x,1})");
      IExpr evaledSolve = e.evaluate(solve);
      System.out.println("evaledRoot 1: " + evaledSolve);
      IExpr evaledPower = e.evaluate(power);
      System.out.println("evaledPower: " + evaledPower);

      solve = e.parse("Solve(x==" + term1 + ", x)");
      evaledSolve = e.evaluate(solve);
      System.out.println("evaledSolve 2: " + evaledSolve);

      IExpr nsolve = e.parse("NSolve(x==" + term1 + ", x)");
      IExpr evaledNSolve = e.evaluate(nsolve);
      System.out.println("evaledNSolve 3: " + evaledNSolve);

This results in:

numericMode: false
numericPrecision: 15
evaledRoot 1: {x->1.4537589480362972E-34}
evaledPower: 1.45376*10^-34
evaledSolve 2: {{x->0.0}}
evaledNSolve 3: {{x->1.4537589480362972E-34}}

I also want to mention that we process the IAST/IExpr objects directly in Java and not as strings so at least in this case it is not relevant how they are printed. Nevertheless the double values in the Num objects are zero so it corresponds to the string-display and in general an accurate toString conversion is of course desired.

HannesWell avatar Jan 31 '22 11:01 HannesWell

With the latest changes the input passed to the EvalEngine can be exactly rationalized before being evaluated using a code like the following:

    String term1 = "1.29600*10^-17";
    String term2 = "(3.6*10^-9)^2";
    String term3 = "(1 / (4 * pi * 3 * 10^6 * 2.2 * 10^9))^2";

    EvalEngine engine = new EvalEngine(true);
    IExpr solve = engine.parse("Solve(x==" + term3 + ",x)");
    solve = F.subst(solve, e -> {
      if (e.isInexactNumber() && e.isReal()) {
        double value = e.evalDouble();
        return F.fractionExact(value, false);
      }
      return F.NIL;
    });
    IExpr solutionSets = engine.evaluateNIL(solve);
    System.out.println("Solution-sets: " + solutionSets);
    IExpr solution = solutionSets.getAt(1).getAt(1).getAt(2);
    System.out.println("solution: " + solution.evalDouble());

@axkr do you plan to use this method for rationalization within the Solve command or at least to make this available with a specialized Rationalization Symja command? For our application this is not critical since I plan to perform the rationalization at compile-time when we parse our equations and before we generate the source code to create the AST.

HannesWell avatar Feb 04 '22 13:02 HannesWell

do you plan to use this method for rationalization within the Solve command or at least to make this available with a specialized Rationalization Symja command? For our application this is not critical since I plan to perform the rationalization at compile-time when we parse our equations and before we generate the source code to create the AST.

Yes please, you can replace the call to NumberTheory.rationalize() in SolveUtils.java.

axkr avatar Feb 04 '22 13:02 axkr

Close for now. Please reopen if necessary.

axkr avatar Nov 03 '23 16:11 axkr