From 87b8f000b90076f94b036d653fbe828d75411119 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 13 Nov 2026 13:46:18 +0000 Subject: [PATCH] Over the reals, answer an exponential equation with real roots only A positive base to a real power is positive, so over the reals a^u = v has a root only for a positive v. Powf's inversion answered log(a, v) for every v, and ln(-1), which is i pi, stayed a node that nothing read as non-real. Under the real codomain the inversion now has no root where an enclosure of v's value shows it is not a positive real; a v it cannot separate from the positive reals, or a symbolic one, keeps its root as before. The replacement search reads its replacement as real, which holds only where the subtree it stands for is real at every real x. One that is not, such as i x, is solved over the complex numbers, so e^(i x) = -1 keeps its root pi; one that is, such as x^2, and has no real root, leaves x with none: e^(x^2) = -1 is { }. The complex codomain is unchanged. BREAKING-CHANGES.md rows measured on the 2.6.0 package and on this branch. Closes #2425. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 26 ++++++++++ .../AnalyticalEquationSolver.cs | 22 ++++++++- .../EquationSolver/InvertNode.Classes.cs | 34 +++++++++++++ .../UnitTests/Common/SolverRegressionTest.cs | 49 +++++++++++++++++++ 4 files changed, 129 insertions(+), 2 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a70ca6bd5..937caf4e6 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -374,6 +374,32 @@ included; a two-sided limit at a finite point is not changed. Found by limitchec | `"x * (ln(-tanh(x)) + 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `0` in 3.2 s | `0` in 0.44 s | | `"sin(x) * (ln(-tanh(x)) + 1/x)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `1` in 3.7 s | `1` in 1.1 s | +### Over the reals, an exponential equation has no root where the power would have to be non-positive + +**Silent: under `MathS.Settings.Codomain.Set(Domain.Real)`, a root that is a logarithm of a number that is not a +positive real is dropped.** A positive base to a real power is positive, so `a^u = v` has a real `u` only for a +positive `v`, and `ln(-1)` is `i pi`. Decided on an enclosure of `v`'s value, so a `v` that cannot be told from the +positive reals keeps its root, and a symbolic one keeps it as before (`e^x = y` is still `{ ln(y) }`). An equation +solved through a replacement that is real at every real `x`, such as `x^2`, and whose replacement has no real root, +has none either. A replacement that is not real at a real `x`, such as `i x`, is solved over the complex numbers, +so `e^(i x) = -1` keeps its root `pi`. The default complex codomain is not changed. +[#2425](https://github.com/asc-community/AngouriMath/issues/2425) + +| Input, with `Codomain` set to `Domain.Real` | Was (2.6.0) | Now | +|---|---|---| +| `"e^x + 1 = 0".Solve("x")` | `{ ln(-1) }` | `{ }` | +| `"e^x - 2e^(-x) - 1 = 0".Solve("x")` | `{ ln(-1), ln(2) }` | `{ ln(2) }` | +| `"2^x = -3".Solve("x")` | `{ log(2, -3) }` | `{ }` | +| `"(1/2)^x = -4".Solve("x")` | `{ log(1/2, -4) }` | `{ }` | +| `"3 e^x + 5 = 0".Solve("x")` | `{ ln(-5/3) }` | `{ }` | +| `"e^(2x + 1) = -3".Solve("x")` | `{ (ln(-3) - 1) / 2 }` | `{ }` | +| `"e^(2x) + e^x - 2 = 0".Solve("x")` | `{ 0, ln(-2) }` | `{ 0 }` | +| `"e^x + e^(-x) = 1".Solve("x")` | `{ ln((1 - sqrt(-3)) / 2), ln((1 + sqrt(-3)) / 2) }` | `{ }` | +| `"e^x - e^(-x) = 3".Solve("x")` | `{ ln((3 - sqrt(13)) / 2), ln((3 + sqrt(13)) / 2) }` | `{ ln((3 + sqrt(13)) / 2) }` | +| `"e^x = -1 + i".Solve("x")` | `{ ln(-1 + i) }` | `{ }` | +| `"e^(x^2) = -1".Solve("x")` | `{ sqrt(ln(-1)), -sqrt(ln(-1)) }` | `{ }` | +| `"e^(x^2) + e^(-x^2) = 1".Solve("x")` | `{ sqrt(ln((1 - sqrt(-3)) / 2)), -sqrt(ln((1 - sqrt(-3)) / 2)), sqrt(ln((1 + sqrt(-3)) / 2)), -sqrt(ln((1 + sqrt(-3)) / 2)) }` | `{ }` | + ### An equation through a negative whole power is not answered with no roots **Silent: an empty answer becomes the roots.** Inverting `x^n = v` for a negative whole `n` multiplied by the roots diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs index efe95d9f5..f03b51ab0 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs @@ -318,6 +318,7 @@ bool ProductIsDefinedAt(Entity root) var newVar = Variable.CreateTemp(expr.Vars); // Here we find all possible replacements and find one that has at least one solution Set? unsettled = null; + var noRealReplacement = false; foreach (var alt in SpellingsToSolveOver(expr)) { MultithreadingFunctional.ExitIfCancelled(); @@ -328,8 +329,23 @@ bool ProductIsDefinedAt(Entity root) var minimumSubtree = TreeAnalyzer.GetMinimumSubtree(alt, x); if (minimumSubtree == x) continue; - // Here we are trying to solve for this replacement - var solutionsSet = Solve(alt.Substitute(minimumSubtree, newVar), newVar).InnerSimplified; + // Here we are trying to solve for this replacement. Over the reals the + // replacement is real only where the subtree it stands for is real at every + // real x: i x is not, and e^u = -1 has the real root pi for u = i x. Where it + // is not, the replacement is solved over the complex numbers, and the roots + // in x are read over the reals when they are solved for below. + // https://github.com/asc-community/AngouriMath/issues/2425 + // Where it is, a replacement with no real root is an x with none: e^(x^2) = -1. + Entity solutionsSet; + var readOverTheReals = MathS.Settings.Codomain.Value == Core.Domain.Real; + var realReplacement = readOverTheReals && TreeAnalyzer.IsRealValued(minimumSubtree, x); + if (readOverTheReals && !realReplacement) + using (MathS.Settings.Codomain.Set(Core.Domain.Complex)) + solutionsSet = Solve(alt.Substitute(minimumSubtree, newVar), newVar).InnerSimplified; + else + solutionsSet = Solve(alt.Substitute(minimumSubtree, newVar), newVar).InnerSimplified; + if (realReplacement && solutionsSet is FiniteSet { IsSetEmpty: true }) + noRealReplacement = true; if (solutionsSet is FiniteSet { IsSetEmpty: false } enums) { var solutions = enums.Select(solution => Solve(minimumSubtree - solution, x, compensateSolving: true)).Unite().InnerSimplified; @@ -346,6 +362,8 @@ bool ProductIsDefinedAt(Entity root) } if (unsettled is { } condition) return condition; + if (noRealReplacement) + return Set.Empty; // // // } diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs index c9bc0017e..b7543b88c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs @@ -127,12 +127,46 @@ partial record Powf : null; return principal; } + // Over the reals a positive base to a real power is positive, so a value that is not + // a positive real is no power of it: e^x = -1 has no real root, and log(e, -1) is + // i pi. https://github.com/asc-community/AngouriMath/issues/2425 + if (MathS.Settings.Codomain.Value == Core.Domain.Real && IsNotAPowerOverTheReals(value, x)) + return Enumerable.Empty(); // a ^ x = value => x = log(a, value) // TODO: determine how we should return periodic roots as // (e ^ x) ^ a != (e ^ a) ^ x for logs return Exponent.Invert(MathS.Log(Base, value), x); } + /// + /// Whether, for a positive real base and an exponent real at every real + /// , is decidedly not a positive real. + /// + /// + /// Decided on an enclosure of the value, never on its printed form: ln(-1) stays + /// a node, and only its value says it is i pi. A value is not a positive real + /// where its imaginary part is enclosed away from zero, where its real part is enclosed + /// below zero whatever its imaginary part is, or where it is exactly zero. Anything + /// else -- a value the enclosure cannot separate from the positive reals, or one with a + /// free variable -- is left to the inversion as before, so this only ever removes a + /// root it has proved is not one. + /// + private bool IsNotAPowerOverTheReals(Entity value, Entity x) + { + if (x is not Variable variable || value.ContainsNode(x) + || !TreeAnalyzer.IsRealValued(Exponent, variable) + || Numerics.PreciseEvaluation.Of(Base, EnclosureDigits) is not { } @base + || !@base.Im.IsZero || @base.Re.Low.Sign <= 0) + return false; + if (value.Evaled is Integer { IsZero: true }) + return true; + if (Numerics.PreciseEvaluation.Of(value, EnclosureDigits) is not { } enclosure) + return false; + return enclosure.Im.Mignitude.Sign > 0 || enclosure.Re.High.Sign < 0; + } + + private const int EnclosureDigits = 40; + // The principal power z^p is e^(p ln z), so its argument is p arg(z): in (-p pi, p pi] // for 0 < p < 1, and in [p pi, -p pi) for -1 < p < 0, where it is never 0. A value // outside that sector is no power of anything, and v^(1/p) is then a number whose diff --git a/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs b/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs index 93f615963..905dd8548 100644 --- a/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs @@ -243,5 +243,54 @@ public void Issue812_ASymbolicRightHandSideCarriesTheCondition() var printed = "abs(x) = a".ToEntity().Solve("x").Stringize(); Assert.Contains("a >= 0", printed); } + // https://github.com/asc-community/AngouriMath/issues/2425 + // Over the reals a positive base to a real power is positive, so e^x = t has a real + // root only for a positive t. e^x + 1 = 0 was answered { ln(-1) }, which is i pi, and + // e^x - 2e^(-x) - 1 = 0, a quadratic in e^x with the roots 2 and -1, kept ln(-1) beside + // ln(2). Each root is checked to be real and to satisfy the equation, and the count says + // that none was lost. + [Theory] + [InlineData("e^x + 1 = 0", 0)] + [InlineData("3 e^x + 5 = 0", 0)] + [InlineData("2^x = -3", 0)] + [InlineData("e^(2x + 1) = -3", 0)] + [InlineData("e^x - 2e^(-x) - 1 = 0", 1)] + [InlineData("e^(2x) + e^x - 2 = 0", 1)] + [InlineData("e^x - e^(-x) = 3", 1)] + [InlineData("e^x + e^(-x) = 1", 0)] + [InlineData("e^x + e^(-x) = 3", 2)] + [InlineData("e^(2x) - 3e^x + 2 = 0", 2)] + [InlineData("e^(x^2) = -1", 0)] + [InlineData("e^(x^2) + e^(-x^2) = 1", 0)] + // A replacement that is not real at a real x is solved over the complex numbers: + // e^(i x) = -1 has the real root pi. + [InlineData("e^(i x) = -1", 1)] + public void Issue2425_AnExponentialEquationOverTheRealsHasOnlyRealRoots(string equation, int count) + { + using var _ = MathS.Settings.Codomain.Set(AngouriMath.Core.Domain.Real); + var statement = (Entity.Equalsf)equation.ToEntity(); + var roots = Assert.IsType(statement.Solve("x")); + Assert.Equal(count, roots.Count); + foreach (var root in roots) + { + var value = Assert.IsAssignableFrom(root.Evaled); + Assert.True(value.ImaginaryPart.EDecimal.Abs().CompareTo(PeterO.Numbers.EDecimal.FromString("1E-30")) < 0, + $"{root} = {value} is not real"); + var residual = Assert.IsAssignableFrom( + (statement.Left - statement.Right).Substitute("x", root).Evaled); + Assert.True(residual.Abs().EDecimal.CompareTo(PeterO.Numbers.EDecimal.FromString("1E-30")) < 0, + $"{root} leaves {residual}"); + } + } + + // The complex reading is untouched: e^x + 1 = 0 has the root i pi there. + [Fact] + public void Issue2425_OverTheComplexNumbersTheRootIsKept() + { + var statement = (Entity.Equalsf)"e^x + 1 = 0".ToEntity(); + var root = Assert.Single(Assert.IsType(statement.Solve("x"))); + var value = Assert.IsAssignableFrom(root.Evaled); + Assert.Equal(System.Math.PI, value.ImaginaryPart.EDecimal.ToDouble(), 12); + } } }