From 46b3c9d8123dbebd1e0f3c0a23900a5839e916f1 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 1 Nov 2026 16:15:04 +0000 Subject: [PATCH] An equation linear in logarithms is solved by exponentiating An equation in two or more logarithms of x had no route of its own, and came to Newton's method, which answered it in decimals and left out the roots it did not reach: ln(x) + ln(x + 1) = 0 was { 0.618... } and ln(1 + 3x^2) - ln(4x) = 1 was { 3.53 }, without 0.0944. k_1 log_b(P_1) + ... + k_n log_b(P_n) = c, for polynomials P_i, rational k_i and one base, holds only where P_1^(k_1 L) ... P_n^(k_n L) = b^(c L), L being the common denominator of the k_i: a polynomial equation, whose roots are found exactly. Each is put back into the equation as written under the principal logarithm and kept only where it holds, since over the complex numbers ln(u) + ln(v) is ln(u v) only up to 2 pi i: at (-1 - sqrt(5))/2 both logarithms of ln(x) + ln(x + 1) carry i pi, and the sum is 2 pi i. Over the reals each argument is to be positive as well. A residual too small to tell from rounding, or a candidate that is not a root of the polynomial, leaves the equation to the routes after, since the roots the polynomial solver missed would be left out: 3x^3 + 9x^2 + 9x + 1, a shifted cube, comes back as -1 three times until #2315. On 400 random equations of this kind, checked against a Newton search under the principal logarithm, master answers 292, leaves 108 unsolved and misses 25 roots in 22 of its answers; this answers all 400, none unsound and none missing a root. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 15 ++ Sources/.editorconfig | 6 + .../AnalyticalEquationSolver.cs | 8 + .../Solvers/EquationSolver/LogarithmSolver.cs | 172 ++++++++++++++++++ .../SolveTest/LogarithmSumEquationTest.cs | 64 +++++++ 5 files changed, 265 insertions(+) create mode 100644 Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/LogarithmSolver.cs create mode 100644 Sources/Tests/UnitTests/Algebra/SolveTest/LogarithmSumEquationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index e11efd135..0525aa8dc 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -177,6 +177,21 @@ as asked the limit of `ln(f)` again, through a limit node of its own | `"limit(ln(2 + sin(x)), x, +oo)".ToEntity().Simplify()` | stack overflow | `limit(ln(2 + sin(x)), x, +oo)` | | `"limitright(ln(sin(1/x) + 2), x, 0)".ToEntity().Simplify()` | stack overflow | `limitright(ln(2 + sin(1 / x)), x, 0)` | +### An equation linear in logarithms is solved by exponentiating + +**Silent: an equation `k_1 log_b(P_1) + … + k_n log_b(P_n) = c`, with at least two logarithms of polynomials to one +base and rational `k_i`, is answered exactly and completely, where Newton's method answered it in decimals and left out +the roots it did not reach.** Every root is one of the polynomial equation `P_1^(k_1 L) … P_n^(k_n L) = b^(c L)`, for +`L` the common denominator of the `k_i`, and each of those is put back into the equation as written under the +principal logarithm, since over the complex numbers `ln(u) + ln(v)` is `ln(u v)` only up to `2 pi i`. Over the reals +each argument is to be positive as well. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"ln(x) + ln(x + 1) = 0".ToEntity().Solve("x")` | `{ 0.6180339887498947… }` | `{ (-1 + sqrt(5)) / 2 }` | +| `"ln(1 + 3x^2) - ln(4x) = 1".ToEntity().Solve("x")` | `{ 3.529945609977906… }` | `{ (-(-e * 4) - sqrt((-e * 4) ^ 2 - 12)) / 6, (-(-e * 4) + sqrt((-e * 4) ^ 2 - 12)) / 6 }` | +| `"ln(x^2 + 1) - ln(x) = 1".ToEntity().Solve("x")` | `{ 0.4386714553485325…, 2.279610373110512… }` | `{ (e - sqrt((-e) ^ 2 - 4)) / 2, (e + sqrt((-e) ^ 2 - 4)) / 2 }` | + ### Primality is decided by Miller–Rabin and Lucas–Lehmer, and finishes **Silent: a membership in `PP` left open past the machine word is decided where a test proves it, and one that did diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 8edc021c0..16b79d163 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -102,6 +102,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Algebra/ExponentialTwoBasesTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Algebra/SolveTest/LogarithmSumEquationTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Algebra/OneByOneMatrixTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n @@ -444,6 +447,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [AngouriMath/Functions/ExponentialQuotients.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[AngouriMath/Functions/Continuous/Solvers/EquationSolver/LogarithmSolver.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [AngouriMath/Functions/Compilation/CompiledMembership.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs index 3bc79a3cf..8dcbc9707 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs @@ -140,6 +140,14 @@ internal static Set Solve(Entity expr, Variable x, bool compensateSolving = fals && ModulusSolver.SolveOverTheComplexNumbers(expr, x) is { } overTheComplexNumbers) return overTheComplexNumbers; + // A sum of rational multiples of logarithms of polynomials to one base is a polynomial + // equation once exponentiated, and each of that one's roots is put back into this one, + // since over the complex numbers ln(u) + ln(v) is ln(u v) only up to 2 pi i: + // ln(x) + ln(x + 1) = 0 is { (sqrt(5) - 1) / 2 }, which Newton's method found in + // decimals. Asked of the equation as it was asked, as the route above is. + if (!compensateSolving && LogarithmSolver.Solve(expr, x) is { } byExponentiating) + return byExponentiating; + // Whether a candidate root really is one. The loose tolerance that guesses the // rational must not also be what decides this: at 1e-7, x^41 + 6x + 1 accepted // -1/6, whose residual is -1.25e-32 -- small, but the difference between an diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/LogarithmSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/LogarithmSolver.cs new file mode 100644 index 000000000..d9a241110 --- /dev/null +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/LogarithmSolver.cs @@ -0,0 +1,172 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System.Collections.Generic; +using System.Linq; +using PeterO.Numbers; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; +using static AngouriMath.Entity.Set; + +namespace AngouriMath.Functions.Algebra.AnalyticalSolving +{ + /// + /// An equation linear in logarithms of polynomials to one base, + /// k_1 log_b(P_1) + ... + k_n log_b(P_n) = c with rational k_i and at least two + /// logarithms, solved by exponentiating. + /// + /// + /// Every root makes b to either side the same, so it is a root of + /// P_1^(k_1 L) ... P_n^(k_n L) = b^(c L), for L the common denominator of the + /// k_i: a polynomial equation, with finitely many roots, which holds at every root of the + /// original. The converse does not hold. Over the complex numbers log(u) + log(v) is + /// log(u v) only up to a multiple of 2 pi i, and a factor can be zero, so each + /// candidate is put back into the equation as written, under the principal logarithm, and kept + /// only where it holds. ln(x) + ln(x + 1) = 0 is x (x + 1) = 1, whose roots are + /// (-1 +- sqrt(5)) / 2; at the negative one both logarithms carry i pi and their sum + /// is 2 pi i, so the answer is { (sqrt(5) - 1) / 2 }. Newton's method answered it + /// in decimals, and ln(1 + 3x^2) - ln(4x) = 1 with one of its two roots. + /// + internal static class LogarithmSolver + { + /// The highest degree the exponentiated equation may have. + private const int HighestDegree = 8; + + internal static Set? Solve(Entity expr, Variable x) + { + if (expr.Vars.Any(variable => variable != x)) + return null; + var logarithms = new List<(ERational Multiplier, Entity Argument)>(); + Entity constant = Integer.Zero; + Entity? logBase = null; + if (!Gather(expr, ERational.One, x, logarithms, ref constant, ref logBase) || logarithms.Count < 2 || logBase is null) + return null; + + var common = EInteger.One; + foreach (var (multiplier, _) in logarithms) + common = common.Multiply(multiplier.Denominator).Divide(common.Gcd(multiplier.Denominator)); + Entity numerator = Integer.One, denominator = Integer.One; + var degree = 0; + foreach (var (multiplier, argument) in logarithms) + { + // In whole numbers: an ERational product is not reduced, and (1/2) 2 is 2/2. + var power = multiplier.Numerator.Multiply(common.Divide(multiplier.Denominator)); + if (!TreeAnalyzer.TryGetPolynomial(argument, x, out var monomials) + || monomials.Keys.Any(exponent => exponent.Sign < 0) + || monomials.Values.Any(coefficient => coefficient.ContainsNode(x))) + return null; + degree += power.Abs().ToInt32Checked() * monomials.Keys.Max()!.ToInt32Checked(); + if (degree > HighestDegree) + return null; + if (power.Sign > 0) + numerator *= MathS.Pow(argument, Integer.Create(power)); + else + denominator *= MathS.Pow(argument, Integer.Create(power.Negate())); + } + // The sum of the logarithms is -constant, so the product of the powers is b to -constant L. + var target = MathS.Pow(logBase, -constant * Integer.Create(common)).InnerSimplified; + var polynomial = numerator - target * denominator; + if (AnalyticalEquationSolver.Solve(polynomial, x) is not FiniteSet candidates) + return null; + // Each candidate is to be a root of the polynomial: one that is not is the polynomial + // solver failing, and the roots it did not find would be left out, the answer claiming + // there are none. 3x^3 + 9x^2 + 9x + 1 = 0 came back as -1 three times, which is no + // root, while a shifted cube's C was 0. And a polynomial of degree one or more has a + // complex root, so no candidates at all is the same failure. + if (candidates.Count == 0 && degree > 0 && MathS.Settings.Codomain.Value != Core.Domain.Real) + return null; + foreach (var candidate in candidates) + if (candidate.Vars.Any() || polynomial.Substitute(x, candidate).Evaled is not Complex { IsFinite: true } residual || !IsZero(residual)) + return null; + + // Over the reals a logarithm has a value only of a positive number, which the evaluation + // below, over the complex numbers, does not ask. + var overTheReals = MathS.Settings.Codomain.Value == Core.Domain.Real; + var roots = new List(); + foreach (var candidate in candidates) + { + if (candidate.Vars.Any()) + return null; + if (overTheReals) + { + var positive = true; + foreach (var (_, argument) in logarithms) + switch (argument.Substitute(x, candidate).Evaled) + { + case Real { IsPositive: true }: + break; + case Complex value when value is Real || IsClearOfZero(Complex.Create(Integer.Zero, value.ImaginaryPart)): + positive = false; + break; + default: + return null; + } + if (!positive) + continue; + } + switch (expr.Substitute(x, candidate).Evaled) + { + case Rational { IsZero: true }: + case Complex { IsFinite: true } residual when residual is not Rational && IsZero(residual): + roots.Add(candidate); + break; + // A candidate on another branch is off by a multiple of 2 pi i, and one where a + // factor is zero has no value: either is plainly no root. A residual too small + // to tell from rounding is not evidence, and dropping the candidate on it could + // leave a root out, so the equation is not answered. + case Complex { IsFinite: true } residual when !IsClearOfZero(residual): + return null; + case Complex: + break; + default: + return null; + } + } + return (Set)new FiniteSet(roots).InnerSimplified; + } + + private static bool IsClearOfZero(Complex residual) + => residual.Abs().EDecimal.CompareTo(EDecimal.FromString("1E-10")) > 0; + + /// + /// Reads , times , as rational multiples of + /// logarithms of to one constant base, and a constant; + /// where it is anything else. + /// + private static bool Gather(Entity term, ERational sign, Variable x, + List<(ERational Multiplier, Entity Argument)> logarithms, ref Entity constant, ref Entity? logBase) + { + switch (term) + { + case var _ when !term.ContainsNode(x): + constant += Rational.Create(sign) * term; + return true; + case Sumf(var augend, var addend): + return Gather(augend, sign, x, logarithms, ref constant, ref logBase) + && Gather(addend, sign, x, logarithms, ref constant, ref logBase); + case Minusf(var minuend, var subtrahend): + return Gather(minuend, sign, x, logarithms, ref constant, ref logBase) + && Gather(subtrahend, sign.Negate(), x, logarithms, ref constant, ref logBase); + case Mulf(Rational multiplier, var rest): + return Gather(rest, sign.Multiply(multiplier.ERational), x, logarithms, ref constant, ref logBase); + case Mulf(var rest, Rational multiplier): + return Gather(rest, sign.Multiply(multiplier.ERational), x, logarithms, ref constant, ref logBase); + case Divf(var rest, Rational { IsZero: false } divisor): + return Gather(rest, sign.Divide(divisor.ERational), x, logarithms, ref constant, ref logBase); + case Logf(var @base, var argument) when !@base.ContainsNode(x): + if (logBase is null) + logBase = @base; + else if (logBase != @base) + return false; + logarithms.Add((sign, argument)); + return true; + default: + return false; + } + } + } +} diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/LogarithmSumEquationTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/LogarithmSumEquationTest.cs new file mode 100644 index 000000000..73c040ce7 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/LogarithmSumEquationTest.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath.Core; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Set; + +namespace AngouriMath.Tests.Algebra +{ + /// + /// An equation linear in logarithms of polynomials to one base is solved by exponentiating: + /// every root is one of the polynomial equation's, and each of those is put back, since over the + /// complex numbers ln(u) + ln(v) is ln(u v) only up to 2 pi i. Newton's + /// method answered these in decimals, and where it did not reach a root it left it out: + /// ln(1 + 3x^2) - ln(4x) = 1 was { 3.53 }, without 0.0944. Found by a + /// cross-check of equations in logarithms against a Newton search; no issue was filed. + /// + [Trait("Area", "Algebra")] + public sealed class LogarithmSumEquationTest + { + private static void AssertRoots(string equation, params string[] roots) + { + var answer = Assert.IsType(equation.ToEntity().Solve("x")); + Assert.Equal(roots.Length, answer.Count); + foreach (var root in roots) + Assert.Contains(answer, found => Number.IsZero((found - root.ToEntity()).EvalNumerical())); + // Exact, not a decimal from a numerical search. + Assert.DoesNotContain(answer, found => found is Number.Real and not Number.Rational); + } + + [Theory] + // (-1 - sqrt(5)) / 2 makes x(x + 1) = 1 too, and there both logarithms carry i pi. + [InlineData("ln(x) + ln(x + 1) = 0", "(sqrt(5) - 1) / 2")] + [InlineData("ln(1 + 3x^2) - ln(4x) = 1", "(4e - sqrt(16e^2 - 12)) / 6", "(4e + sqrt(16e^2 - 12)) / 6")] + // Both arguments negative: their i pi cancel. + [InlineData("ln(-1 - 3x^2) - ln(4x) = 1", "-(4e - sqrt(16e^2 - 12)) / 6", "-(4e + sqrt(16e^2 - 12)) / 6")] + [InlineData("ln(x^2 + 1) - ln(x) = 1", "(e - sqrt(e^2 - 4)) / 2", "(e + sqrt(e^2 - 4)) / 2")] + // x = -2 makes x(x - 1) = 6, where the sum is ln(6) + 2 pi i. + [InlineData("ln(x) + ln(x - 1) = ln(6)", "3")] + [InlineData("ln(x - 2) + ln(x - 3) = ln(2)", "4")] + // x = -1 makes x^2 = x + 2, where 2 ln(x) is 2 pi i and ln(1) is 0. + [InlineData("2 * ln(x) = ln(x + 2)", "2")] + [InlineData("log(2, x) + log(2, x - 2) = 3", "4")] + [InlineData("ln(x) / 2 + ln(x + 1) = 1", + "-(2 + ((-2 - 27 e^2 + sqrt((-2 - 27 e^2)^2 - 4)) / 2)^(1/3) + 1 / ((-2 - 27 e^2 + sqrt((-2 - 27 e^2)^2 - 4)) / 2)^(1/3)) / 3")] + public void SolvedByExponentiating(string equation, params string[] roots) + => AssertRoots(equation, roots); + + /// Over the reals a logarithm of a negative number has no value, and the root that needs one goes. + [Fact] + public void OverTheRealsTheArgumentsArePositive() + { + using var _ = MathS.Settings.Codomain.Set(Domain.Real); + AssertRoots("ln(1 + 3x^2) - ln(4x) = 1", "(4e - sqrt(16e^2 - 12)) / 6", "(4e + sqrt(16e^2 - 12)) / 6"); + Assert.Equal("{ }".ToEntity(), "ln(-1 - 3x^2) - ln(4x) = 1".ToEntity().Solve("x")); + } + } +}