diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index c3a7d04a2..502ba5734 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -205,6 +205,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 11a4545b4..ab3cafd71 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -105,6 +105,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/SolveTest/ADepressedBiquadraticQuarticTest.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
@@ -450,6 +453,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"));
+ }
+ }
+}