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
13 changes: 13 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ did, and `ln(x + 2) / ln(x + 3)` at 0 was `ln(2) / ln(3)`. With symbols it was n
| `"limit(ln(a + 1/x) / ln(b + 1/x), x, +oo)".ToEntity().Simplify()` | `a / b` | `ln(a) / ln(b)` |
| `"limit(ln(1 + x) / ln(y + x), x, 0)".ToEntity().Simplify()` | `1 / y` | `0 provided not ln(y) = 0` |

### An equation in two different square roots is solved exactly

**Silent: an equation `m_1 sqrt(A) + m_2 sqrt(B) + P = 0` in x alone, with two different square roots, is answered in
radicals, where it was answered in decimals by Newton's method or left unsolved.** It is squared twice into a
polynomial, `4 P^2 m_2^2 B = (m_1^2 A - P^2 - m_2^2 B)^2`, and each of that polynomial's roots is put back into the
equation. Newton's method cannot reach a root where a radicand is 0, and 2.6.0 found the one below only because a
compiled `0^(-1/2)` was 0; since that is `NaN`, the root was missed.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"sqrt(-4 - 4x) + sqrt(2 + x) = -3 - 4x".ToEntity().Solve("x")` | `{ -1.1951718122545800, -1 }` | `{ -1, … }`, the second root in radicals |
| `"sqrt(x + 5) + sqrt(x) = x".ToEntity().Solve("x")` | `{ 5.6349218075063181 }` | the same root in radicals |

### A radical times a power of its variable is solved exactly

**Silent: an equation in `sqrt(x)` or a cube root of `x` beside powers of `x` is answered in radicals and with all its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,29 @@ static bool BasesAreEqual(List<(Entity main, Integer pow)> f1,
return false;
return true;
}
for (int i = 1; i < fractioned.Count; i++)
// Terms under one radical are gathered into one term.
var groups = new List<(Entity multiplier, List<(Entity main, Integer pow)> fracs)>();
foreach (var term in fractioned)
{
if (BasesAreEqual(fractioned[i].fracs, fractioned[0].fracs))
{
var were = fractioned[0];
fractioned[0] = (were.multiplier + fractioned[i].multiplier, were.fracs);
}
var same = groups.FindIndex(group => BasesAreEqual(group.fracs, term.fracs));
if (same < 0)
groups.Add(term);
else
return false;
groups[same] = (groups[same].multiplier + term.multiplier, groups[same].fracs);
}
// Only with no other name in the equation: squaring adds roots, and each is put back
// into the equation to be dropped where it fails, which with a parameter left in cannot
// be decided. sqrt(x + 1) + sqrt(x + 2) + a + x = 0 kept all four roots of its square,
// and at a = 2 none of them is a root.
if (groups.Count == 2 && expr.Vars.Count == 1
&& TwoSquareRootsSquaredAway(groups[0], groups[1], normalPolynom) is { } squared)
{
dst = (Set)AnalyticalEquationSolver.Solve(squared, x).InnerSimplified;
return true;
}
if (groups.Count != 1)
return false;
fractioned = groups;

var (multiplier, fracs) = fractioned[0];

Expand All @@ -120,5 +133,29 @@ static bool BasesAreEqual(List<(Entity main, Integer pow)> f1,
dst = (Set)AnalyticalEquationSolver.Solve(finalExpr, x).InnerSimplified;
return true;
}

/// <summary>
/// <c>m_1 sqrt(A) + m_2 sqrt(B) + P = 0</c>, two different square roots and nothing else
/// fractional, squared until neither is left: <c>m_1^2 A = (P + m_2 sqrt(B))^2</c>, so
/// <c>2 P m_2 sqrt(B) = m_1^2 A - P^2 - m_2^2 B</c>, and squared again
/// <c>4 P^2 m_2^2 B = (m_1^2 A - P^2 - m_2^2 B)^2</c>. Each squaring keeps every root of
/// the equation it squares, so this has all of them, and the roots it adds are not the
/// equation's; the caller puts each back in. Without it two different radicals were not
/// raised at all, and sqrt(-4 - 4x) + sqrt(2 + x) = -3 - 4x went to Newton's method, which
/// answered its root -1.195 and not its root -1, where the first radical's argument is 0.
/// Null where either is not a lone square root.
/// </summary>
private static Entity? TwoSquareRootsSquaredAway(
(Entity multiplier, List<(Entity main, Integer pow)> fracs) first,
(Entity multiplier, List<(Entity main, Integer pow)> fracs) second,
Entity rest)
{
if (first.fracs is not [(var a, Integer(2))] || second.fracs is not [(var b, Integer(2))])
return null;
var m1 = first.multiplier;
var m2 = second.multiplier;
var once = m1 * m1 * a - rest * rest - m2 * m2 * b;
return (4 * rest * rest * m2 * m2 * b - once * once).InnerSimplified;
}
}
}
73 changes: 73 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/TwoSquareRootsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// 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.Linq;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Algebra
{
/// <summary>
/// An equation in two different square roots, <c>m_1 sqrt(A) + m_2 sqrt(B) + P = 0</c>, is squared twice into a
/// polynomial, and each of that polynomial's roots is put back into the equation. Two different radicals were not
/// raised at all, and such an equation went to Newton's method, which cannot reach a root where a radicand is 0:
/// <c>sqrt(-4 - 4x) + sqrt(2 + x) = -3 - 4x</c> was answered without its root -1. No issue was filed; found by a
/// cross-check of random radical equations against mpmath.
/// </summary>
[Trait("Area", "Algebra")]
public sealed class TwoSquareRootsTest
{
private static Entity.Set.FiniteSet Roots(string equation)
=> Assert.IsType<Entity.Set.FiniteSet>(equation.ToEntity().Solve("x"));

// Each answer is a root, put back into the equation it came from.
private static void AreRoots(string equation, Entity.Set.FiniteSet roots)
{
var (left, right) = equation.ToEntity() is Entity.Equalsf(var l, var r) ? (l, r) : (equation.ToEntity(), (Entity)0);
foreach (var root in roots)
Assert.True(((Entity.Number.Complex)(left - right).Substitute("x", root).EvalNumerical()).Abs().EDecimal.CompareTo(PeterO.Numbers.EDecimal.FromString("1e-30")) < 0,
$"{root} is not a root of {equation}");
}

[Fact]
public void ARootWhereARadicandIsZero()
{
const string equation = "sqrt(-4 - 4x) + sqrt(2 + x) = -3 - 4x";
var roots = Roots(equation);
Assert.Contains((Entity)(-1), roots);
Assert.Equal(2, roots.Count);
AreRoots(equation, roots);
}

[Theory]
[InlineData("sqrt(2x + 3) - sqrt(x + 1) = 1", new[] { -1, 3 })]
[InlineData("sqrt(1 - x) + sqrt(x) = 1 - 2x", new[] { 0 })]
[InlineData("sqrt(-4 - 4x) + sqrt(2 + x) = 1", new[] { -1 })]
public void AreTheWholeRoots(string equation, int[] expected)
{
var roots = Roots(equation);
Assert.Equal(expected.Select(root => (Entity)root).ToHashSet(), roots.ToHashSet());
}

[Theory]
[InlineData("sqrt(x + 5) + sqrt(x) = x")]
[InlineData("2 sqrt(x) + 3 sqrt(x + 1) = x")]
public void AnIrrationalRootIsExactAndARoot(string equation)
{
var roots = Roots(equation);
Assert.Single(roots);
AreRoots(equation, roots);
}

// Squared, these are x = x - 1 and x + 1 = x, which have no roots, and neither has the equation.
[Theory]
[InlineData("sqrt(x) + sqrt(x - 1) = 0")]
[InlineData("sqrt(x + 1) - sqrt(x) = 0")]
public void NoRoot(string equation) => Assert.Empty(Roots(equation));
}
}
Loading