Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// An equation linear in logarithms of polynomials to one base,
/// <c>k_1 log_b(P_1) + ... + k_n log_b(P_n) = c</c> with rational <c>k_i</c> and at least two
/// logarithms, solved by exponentiating.
/// </summary>
/// <remarks>
/// Every root makes <c>b</c> to either side the same, so it is a root of
/// <c>P_1^(k_1 L) ... P_n^(k_n L) = b^(c L)</c>, for <c>L</c> the common denominator of the
/// <c>k_i</c>: a polynomial equation, with finitely many roots, which holds at every root of the
/// original. The converse does not hold. Over the complex numbers <c>log(u) + log(v)</c> is
/// <c>log(u v)</c> only up to a multiple of <c>2 pi i</c>, 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. <c>ln(x) + ln(x + 1) = 0</c> is <c>x (x + 1) = 1</c>, whose roots are
/// <c>(-1 +- sqrt(5)) / 2</c>; at the negative one both logarithms carry <c>i pi</c> and their sum
/// is <c>2 pi i</c>, so the answer is <c>{ (sqrt(5) - 1) / 2 }</c>. Newton's method answered it
/// in decimals, and <c>ln(1 + 3x^2) - ln(4x) = 1</c> with one of its two roots.
/// </remarks>
internal static class LogarithmSolver
{
/// <summary>The highest degree the exponentiated equation may have.</summary>
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<Entity>();
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;

/// <summary>
/// Reads <paramref name="term"/>, times <paramref name="sign"/>, as rational multiples of
/// logarithms of <paramref name="x"/> to one constant base, and a constant; <see langword="false"/>
/// where it is anything else.
/// </summary>
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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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 <c>ln(u) + ln(v)</c> is <c>ln(u v)</c> only up to <c>2 pi i</c>. Newton's
/// method answered these in decimals, and where it did not reach a root it left it out:
/// <c>ln(1 + 3x^2) - ln(4x) = 1</c> was <c>{ 3.53 }</c>, without <c>0.0944</c>. Found by a
/// cross-check of equations in logarithms against a Newton search; no issue was filed.
/// </summary>
[Trait("Area", "Algebra")]
public sealed class LogarithmSumEquationTest
{
private static void AssertRoots(string equation, params string[] roots)
{
var answer = Assert.IsType<FiniteSet>(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);

/// <summary>Over the reals a logarithm of a negative number has no value, and the root that needs one goes.</summary>
[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"));
}
}
}
Loading